Table of Contents
Sensors are how our robots and other intelligent devices navigate. Whether you are building a self-driving rover or an intelligent quadcopter, knowing “which way is up” and “which way is North” is useful, sometimes even essential.
Today, we are looking at the Adafruit LSM303AGR, a 6-DOF (Six Degrees of Freedom) module that combines an accelerometer and magnetometer. We will hook it up to a microcontroller, learn to calibrate it correctly with the MotionCal tool, and finally build a compass with an OLED display.
Introduction
The Adafruit LSM303AGR is a compact, low-power multifunction MEMS sensor that combines a 3‑axis accelerometer and a 3‑axis magnetometer on a single breakout. It’s ideal for orientation, tilt, motion detection, and compass projects for makers, robots, drones, and wearables.
Today, we’ll explore how accelerometers and magnetometers work, wire up the sensor to a Seeeduino XIAO ESP32-S3 microcontroller, run through all the example sketches, perform proper magnetometer calibration, and build a complete digital compass with an OLED display. By the end, you’ll have the knowledge and practical experience to incorporate this powerful sensor into your own projects.
The LSM303AGR Multifunction Sensor
The LSM303AGR is a versatile motion-sensing module that combines two essential sensors in one compact package: a 3-axis accelerometer and a 3-axis magnetometer. This powerful combination makes it ideal for applications requiring orientation detection, navigation, and motion tracking.

The LSM303AGR represents the latest iteration in STMicroelectronics’ popular LSM303 series, which has been widely used in consumer electronics, IoT devices, and industrial applications. This updated version offers improved performance, lower power consumption, and enhanced features compared to its predecessors, like the LSM303DLHC.
The LSM303AGR is the modern replacement for older LSM303 variants, such as the LSM303DLHC. It pairs a MEMS (Micro-Electro-Mechanical Systems) capacitive accelerometer with a magnetometer optimized for low noise and low power.
It communicates over I²C or SPI and is available on an Adafruit breakout board with STEMMA QT / Qwiic connectors for easy I²C hookup.
Understanding the Sensor Components
Let’s take a look at the two sensors that are combined within the LSM303AGR module:
What is an Accelerometer?
An accelerometer measures acceleration forces in three perpendicular axes (X, Y, and Z). Importantly, it measures both dynamic acceleration (motion) and static acceleration (gravity). When sitting still on a table, an accelerometer primarily detects Earth’s gravity—approximately 9.8 m/s² or 1g. This constant gravitational reference allows accelerometers to determine orientation. Tilt the sensor, and the gravity vector distributes differently across the three axes, revealing the sensor’s angle relative to the ground.

The LSM303AGR uses MEMS (Micro-Electro-Mechanical Systems) capacitive sensing technology. Inside the chip, microscopic mechanical structures etched from silicon act as tiny spring-mass systems.

When acceleration occurs, a proof mass moves, changing the capacitance between fixed and moving plates. This capacitance change is measured and converted into acceleration values.
What is a Magnetometer?
A magnetometer measures the strength and direction of magnetic fields in three axes. In navigation applications, magnetometers detect Earth’s magnetic field, which ranges from 25 to 65 microTesla (µT) depending on your location. By determining which way the magnetic field points, we can find magnetic north—the fundamental principle behind all compasses, from ancient lodestones to modern digital versions.

The LSM303AGR’s magnetometer (the LIS2MDL component) uses magnetoresistive sensing technology. Certain materials change their electrical resistance when exposed to magnetic fields. By measuring these resistance changes in three perpendicular directions, the sensor can determine both the magnitude and direction of the magnetic field.
Combining the two sensors
When you combine an accelerometer and a magnetometer, you get a complete orientation sensing system. The accelerometer tells you which way is down (via gravity), and the magnetometer tells you which way is north. Together, they provide full 3D orientation awareness—perfect for navigation, augmented reality, robotics, and countless other applications.
Adafruit LSM303AGR Module
The Adafruit LSM303AGR module is probably the easiest way to experiment with this sensor. The module incorporates the STMicroelectronics LSM303AGR sensor, a voltage regulator, and some support circuitry. It exposes the I²C connections (SDA and SCL) and several interrupts for both the accelerometer and magnetometer.
The module can operate with either 3.3 or 5-volt logic devices.
Here is the pinout of the module:

Note that the module includes orientation arrows indicating the directions of the X and Y axes. This is important to know when using the module.

Let’s see how we can use this sensor module with a microcontroller.
Hooking up the Sensor
The LSM303AGR is very easy to use with just about any microcontroller, as it is compatible with both 3.3-volt and 5-volt logic. For these experiments, I have chosen a Seeeduino XIAO ESP32-S3, but there is nothing in particular about the GSI microcontroller that makes it uniquely suitable for the LSM303AGR. You could easily substitute another microcontroller. If you do, you may have to alter the code to accommodate any different pinouts; otherwise, the code and hookup are identical.
Here is how I hooked up my Seeeduino XIAO ESP32-S3 to the LSM303AGR:

Now that we have it hooked up, we can begin to experiment with the two internal sensors. We will start with the accelerometer.
Using the Accelerometer

Let’s explore the accelerometer functionality. We’ll start by installing the necessary libraries, then run the example sketch to see the accelerometer in action.
Open the Arduino IDE and navigate to the Library Manager. Look for the Adafruit_LSM303_Accel library and install it. You will be prompted to install additional libraries, such as the Adafruit Unified Sensor library (which you may already have installed). Click to install all the required libraries.
Before diving into code, let’s understand what the accelerometer actually measures.
When an accelerometer sits motionless on a table, it’s not measuring zero acceleration—it’s measuring Earth’s gravity! The downward-facing axis reads approximately 9.8 m/s² (or 1g in convenient units). This might seem counterintuitive, but remember: an object at rest on Earth’s surface is being constantly accelerated upward by the table pushing against gravity.
If the accelerometer sits flat with the Z-axis pointing up:
- X-axis: ~0 m/s² (no gravity component in horizontal plane)
- Y-axis: ~0 m/s² (no gravity component in horizontal plane)
- Z-axis: +9.8 m/s² (full gravity force pointing up through the sensor)
Tilt the sensor 45 degrees forward, and the gravity vector distributes:
- X-axis: ~6.9 m/s² (gravity component pulling forward)
- Y-axis: ~0 m/s²
- Z-axis: ~6.9 m/s² (gravity component still pointing up)
The total magnitude remains constant: √(6.9² + 6.9²) ≈ 9.8 m/s²
When you move the sensor, you’ll see readings change as motion-induced acceleration adds to or subtracts from the gravitational constant. This is why accelerometers are perfect for detecting both orientation (via gravity) and motion (via changes in acceleration).
Accelerometer Code
A simple way to test the accelerometer is to use the sample sketch included with the Adafruit library. You will find it at: File → Examples → Adafruit LSM303 Accel → accelsensor.ino
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
#include <Adafruit_LSM303_Accel.h> #include <Adafruit_Sensor.h> #include <Wire.h> /* Assign a unique ID to this sensor at the same time */ Adafruit_LSM303_Accel_Unified accel = Adafruit_LSM303_Accel_Unified(54321); void displaySensorDetails(void) { sensor_t sensor; accel.getSensor(&sensor); Serial.println("------------------------------------"); Serial.print("Sensor: "); Serial.println(sensor.name); Serial.print("Driver Ver: "); Serial.println(sensor.version); Serial.print("Unique ID: "); Serial.println(sensor.sensor_id); Serial.print("Max Value: "); Serial.print(sensor.max_value); Serial.println(" m/s^2"); Serial.print("Min Value: "); Serial.print(sensor.min_value); Serial.println(" m/s^2"); Serial.print("Resolution: "); Serial.print(sensor.resolution); Serial.println(" m/s^2"); Serial.println("------------------------------------"); Serial.println(""); delay(500); } void setup(void) { Serial.begin(9600); Serial.println("Accelerometer Test"); Serial.println(""); /* Initialise the sensor */ if (!accel.begin()) { /* There was a problem detecting the ADXL345 ... check your connections */ Serial.println("Ooops, no LSM303 detected ... Check your wiring!"); while (1) ; } /* Display some basic information on this sensor */ displaySensorDetails(); accel.setRange(LSM303_RANGE_4G); Serial.print("Range set to: "); lsm303_accel_range_t new_range = accel.getRange(); switch (new_range) { case LSM303_RANGE_2G: Serial.println("+- 2G"); break; case LSM303_RANGE_4G: Serial.println("+- 4G"); break; case LSM303_RANGE_8G: Serial.println("+- 8G"); break; case LSM303_RANGE_16G: Serial.println("+- 16G"); break; } accel.setMode(LSM303_MODE_NORMAL); Serial.print("Mode set to: "); lsm303_accel_mode_t new_mode = accel.getMode(); switch (new_mode) { case LSM303_MODE_NORMAL: Serial.println("Normal"); break; case LSM303_MODE_LOW_POWER: Serial.println("Low Power"); break; case LSM303_MODE_HIGH_RESOLUTION: Serial.println("High Resolution"); break; } } void loop(void) { /* Get a new sensor event */ sensors_event_t event; accel.getEvent(&event); /* Display the results (acceleration is measured in m/s^2) */ Serial.print("X: "); Serial.print(event.acceleration.x); Serial.print(" "); Serial.print("Y: "); Serial.print(event.acceleration.y); Serial.print(" "); Serial.print("Z: "); Serial.print(event.acceleration.z); Serial.print(" "); Serial.println("m/s^2"); /* Delay before the next sample */ delay(500); } |
Note that this code has been modified from the provided example; I have removed the lines that made it work only on an ESP8266.
Accelerometer Demo
Load the code to the XIAO and open the serial monitor. You should observe the acceleration in the X, Y, and Z axes.

Note that the Z-axis acceleration value is the acceleration of gravity.
Using the Magnetometer

Now we will turn our attention to the magnetometer in the LSM303AGR. While accelerometers are relatively forgiving, magnetometers require careful calibration to achieve accurate results. Let’s explore how the magnetometer works and how to use it effectively.
How Magnetometers Work (and why they need calibration)
Our planet acts like a giant bar magnet with field lines emerging from the magnetic south pole (near geographic north) and entering the magnetic north pole (near geographic south). This field is relatively weak—typically 25 to 65 microTesla (µT) depending on your location.

The magnetometer measures magnetic field strength in three perpendicular directions (X, Y, Z). By analyzing these three components, we can determine:
- The direction to magnetic north
- The strength of the local magnetic field
- The presence of nearby magnets or ferromagnetic materials
Unlike gravity (which is remarkably constant), Earth’s magnetic field is easily overwhelmed by local magnetic sources:
- Permanent magnets: Speakers, motors, magnetic clasps
- Ferromagnetic materials: Iron, steel, nickel (distort the field without being magnetic themselves)
- Electromagnetic interference: Power supplies, transformers, current-carrying wires
This sensitivity makes magnetometer calibration essential for accurate compass readings.
Note that the magnetic North and South poles are not the same as the geographical ones. To obtain the true direction, you will need to add a “deviation factor” to your compass readings. Here are a few examples:
- New York: ~13° West
- San Francisco: ~14° East
- London: ~0°
- Sydney: ~12° East
This applies to both electronic and standard compasses.
Adafruit LIS2MDL Library
To work with the magnetometer in the LSM303AGR, we will use another Adafruit library, the Adafruit LIS2MDL library. As with the accelerometer library, you can install it using the Library Manager in the Arduino IDE.
This library also depends upon other libraries, which will be installed at the same time.
Magnetometer Calibration
To get useful data, we must calibrate the sensor to remove:
- Hard Iron distortions: Permanent magnetic offsets (like magnetized screws or the sensor package itself).
- Soft Iron distortions: Warping of the magnetic field caused by nearby ferrous metals.
We will use the MotionCal tool by Paul Stoffregen. To use it, we need to send raw data in a specific format to our computer. I have written a custom sketch for this.
Step 1: Upload the Calibration Sketch
Copy the code below (or use the ZIP file with all of the code) and upload it to your XIAO.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
/* MotionCal Magnetometer Calibration motioncal_calibration.ino Outputs sensor data in the format required by MotionCal Use with Adafruit LSM303AGR on Seeeduino XIAO ESP32-S3 Hardware Connections: LSM303AGR: VIN -> 3.3V GND -> GND SCL -> D5 (GPIO6) SDA -> D4 (GPIO5) Instructions: 1. Upload this sketch to your XIAO ESP32-S3 2. Open Serial Monitor at 115200 baud - you should see data streaming 3. Close Arduino Serial Monitor (MotionCal needs exclusive access) 4. Open MotionCal application 5. Select your COM port in MotionCal 6. Slowly rotate the sensor in all directions (figure-8 pattern works well) 7. Watch the sphere fill up in MotionCal 8. When complete, note the calibration values shown Required Libraries (install via Arduino Library Manager): - Adafruit_LSM303_Accel - Adafruit_LIS2MDL - Adafruit Unified Sensor - Adafruit BusIO DroneBot Workshop 2025 https://dronebotworkshop.com */ #include <Wire.h> #include <Adafruit_LSM303_Accel.h> #include <Adafruit_LIS2MDL.h> #include <Adafruit_Sensor.h> // Create sensor objects Adafruit_LSM303_Accel_Unified accel = Adafruit_LSM303_Accel_Unified(54321); Adafruit_LIS2MDL mag = Adafruit_LIS2MDL(12345); void setup() { Serial.begin(115200); while (!Serial) delay(10); // Wait for serial port to connect delay(100); Serial.println("MotionCal Magnetometer Calibration"); Serial.println("==================================="); // Initialize I2C Wire.begin(); delay(100); // Initialize accelerometer Serial.print("Initializing accelerometer... "); if (!accel.begin()) { Serial.println("FAILED!"); Serial.println("Check wiring and connections."); while(1) delay(1000); } Serial.println("OK"); // Initialize magnetometer Serial.print("Initializing magnetometer... "); if (!mag.begin()) { Serial.println("FAILED!"); Serial.println("Check wiring and connections."); while(1) delay(1000); } Serial.println("OK"); // Configure sensors for optimal calibration accel.setRange(LSM303_RANGE_4G); accel.setMode(LSM303_MODE_NORMAL); mag.setDataRate(LIS2MDL_RATE_100_HZ); Serial.println("==================================="); Serial.println("Sensors initialized successfully!"); Serial.println(); Serial.println("INSTRUCTIONS:"); Serial.println("1. Close this Serial Monitor"); Serial.println("2. Open MotionCal application"); Serial.println("3. Select your COM port"); Serial.println("4. Rotate sensor in all directions"); Serial.println("5. Fill the sphere completely"); Serial.println("6. Note calibration values when done"); Serial.println("==================================="); Serial.println(); delay(3000); Serial.println("Starting data stream..."); Serial.println(); // Give MotionCal a moment to connect delay(500); } void loop() { sensors_event_t accel_event, mag_event; // Read sensor data accel.getEvent(&accel_event); mag.getEvent(&mag_event); // MotionCal expects data in this format: // Raw:ax,ay,az,gx,gy,gz,mx,my,mz // Where values are integers (or integer-like) // Convert accelerometer from m/s² to "raw-ish" integer values // Multiply by 100 to get reasonable integer values int ax = (int)(accel_event.acceleration.x * 100); int ay = (int)(accel_event.acceleration.y * 100); int az = (int)(accel_event.acceleration.z * 100); // Gyroscope values (we don't have a gyro, so send zeros) int gx = 0; int gy = 0; int gz = 0; // Convert magnetometer from µT to "raw-ish" integer values // Multiply by 10 to get reasonable integer values int mx = (int)(mag_event.magnetic.x * 10); int my = (int)(mag_event.magnetic.y * 10); int mz = (int)(mag_event.magnetic.z * 10); // Output in MotionCal format Serial.print("Raw:"); Serial.print(ax); Serial.print(","); Serial.print(ay); Serial.print(","); Serial.print(az); Serial.print(","); Serial.print(gx); Serial.print(","); Serial.print(gy); Serial.print(","); Serial.print(gz); Serial.print(","); Serial.print(mx); Serial.print(","); Serial.print(my); Serial.print(","); Serial.print(mz); Serial.println(); // Also output in "Uni" format (unified sensor format with actual units) // This is helpful for debugging but MotionCal uses the Raw: line Serial.print("Uni:"); Serial.print(accel_event.acceleration.x, 2); Serial.print(","); Serial.print(accel_event.acceleration.y, 2); Serial.print(","); Serial.print(accel_event.acceleration.z, 2); Serial.print(","); Serial.print("0.00,0.00,0.00,"); // No gyro Serial.print(mag_event.magnetic.x, 2); Serial.print(","); Serial.print(mag_event.magnetic.y, 2); Serial.print(","); Serial.print(mag_event.magnetic.z, 2); Serial.println(); // Update rate - 10Hz is good for MotionCal delay(100); } |
This sketch initializes the accelerometer and magnetometer, sets the magnetometer data rate to 100Hz for smoother readings, and streams data in the Raw:ax,ay,az,gx,gy,gz,mx,my,mz format. Since the LSM303AGR has no gyroscope, we send zeros for the middle three values.
Step 2: Run MotionCal
- Close the Arduino Serial Monitor (MotionCal needs the port).
- Open the MotionCal application on your PC.
- Select your XIAO’s COM port.
You should see the sensor data visually represented as points in 3D space.

Rotate the sensor in all directions. A “figure-8” motion works best. Your goal is to fill the sphere with red dots.
Once the fit error is low (typically < 3%), look at the Magnetic Offset (Hard Iron) values on the screen. Write these down! You will need them for the compass.
Here is an example of a calibration that I did on my workbench, which is probably not a great place to do it, as there are several ferromagnetic sources nearby.

It took me about 15 minutes to complete the calibration.
Testing the Magnetometer
The Adafruit LIS2MDL library comes with a few example sketches. A good one to start with is the magsensor.ino sketch, which outputs values from all three magnetometer axes. You can find it at File → Examples → Adafruit LIS2MDL → magsensor.ino
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include <Adafruit_LIS2MDL.h> #include <Adafruit_Sensor.h> #include <Wire.h> /* Assign a unique ID to this sensor at the same time */ Adafruit_LIS2MDL lis2mdl = Adafruit_LIS2MDL(12345); #define LIS2MDL_CLK 13 #define LIS2MDL_MISO 12 #define LIS2MDL_MOSI 11 #define LIS2MDL_CS 10 void setup(void) { Serial.begin(115200); while (!Serial) delay(10); // will pause Zero, Leonardo, etc until serial console opens Serial.println("LIS2MDL Magnetometer Test"); Serial.println(""); /* Enable auto-gain */ lis2mdl.enableAutoRange(true); /* Initialise the sensor */ if (!lis2mdl.begin()) { // I2C mode //if (! lis2mdl.begin_SPI(LIS2MDL_CS)) { // hardware SPI mode //if (! lis2mdl.begin_SPI(LIS2MDL_CS, LIS2MDL_CLK, LIS2MDL_MISO, LIS2MDL_MOSI)) { // soft SPI /* There was a problem detecting the LIS2MDL ... check your connections */ Serial.println("Ooops, no LIS2MDL detected ... Check your wiring!"); while (1) delay(10); } /* Display some basic information on this sensor */ lis2mdl.printSensorDetails(); } void loop(void) { /* Get a new sensor event */ sensors_event_t event; lis2mdl.getEvent(&event); /* Display the results (magnetic vector values are in micro-Tesla (uT)) */ Serial.print("X: "); Serial.print(event.magnetic.x); Serial.print(" "); Serial.print("Y: "); Serial.print(event.magnetic.y); Serial.print(" "); Serial.print("Z: "); Serial.print(event.magnetic.z); Serial.print(" "); Serial.println("uT"); /* Note: You can also get the raw (non unified values) for */ /* the last data sample as follows. The .getEvent call populates */ /* the raw values used below. */ // Serial.print("X Raw: "); Serial.print(lis2mdl.raw.x); Serial.print(" "); // Serial.print("Y Raw: "); Serial.print(lis2mdl.raw.y); Serial.print(" "); // Serial.print("Z Raw: "); Serial.print(lis2mdl.raw.z); Serial.println(""); /* Delay before the next sample */ delay(100); } |
This sketch reads the magnetometer and displays the field strength in microTesla.

A good way to see how the sensor works is to observe the output on the serial monitor and place a magnet near the sensor, as I have done in the illustration above. You will see the sensor respond instantly to the magnet’s magnetic field.
Simple Compass Function
Now that we understand both sensors and have calibrated the magnetometer, let’s create a basic compass. The fundamental principle is simple: Earth’s magnetic field points north, and by measuring this field in the horizontal plane, we can calculate heading.
Simple Compass Code
When we installed the Adafruit LIS2MDL library, four example sketches were added to the Arduino IDE. We have already looked at one of them; now we’ll use another to create a compass that displays in the serial monitor.
The example sketch is compass.ino. It can be found at File → Examples → Adafruit LIS2MDL → compass.ino.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_LIS2MDL.h> Adafruit_LIS2MDL mag = Adafruit_LIS2MDL(12345); void setup(void) { Serial.begin(115200); Serial.println("Magnetometer Test"); Serial.println(""); /* Initialise the sensor */ if(!mag.begin()) { /* There was a problem detecting the LIS2MDL ... check your connections */ Serial.println("Ooops, no LIS2MDL detected ... Check your wiring!"); while(1); } } void loop(void) { /* Get a new sensor event */ sensors_event_t event; mag.getEvent(&event); float Pi = 3.14159; // Calculate the angle of the vector y,x float heading = (atan2(event.magnetic.y,event.magnetic.x) * 180) / Pi; // Normalize to 0-360 if (heading < 0) { heading = 360 + heading; } Serial.print("Compass Heading: "); Serial.println(heading); delay(500); } |
Make sure to apply the magnetic offset values from the calibration procedure; this will improve the compass’s accuracy.
Simple Compass Demo
Load the sketch to the XIAO and observe the output on the serial monitor.

Move the board around, and you should see the direction indicated on the serial monitor.
This sketch can serve as the basis for a navigation system for robotics and other applications.
OLED Compass
We will expand upon the previous sketch and create a compass with the traditional circular display. To keep it simple, we will use a small OLED display. For a more practical project, you could try substituting a larger display.
OLED Compass Hookup
The OLED hookup is straightforward. We will expand the circuit we have been working with and add an SSD1306 OLED display using its I²C bus connections.
Here is the final hookup:

Note that it is also possible to power the OLED using the 5-volt output from the Seeeduino XIAO ESP32-S3 board.
OLED Compass Code
Here is the code we will use for our OLED compass. Note that you will need to add the Adafruit GFX and SSD1306 libraries to your Arduino IDE; they are available in the Library Manager.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
/* OLED Compass oled_compass.ino Electronic Compass with OLED Display Use Adafruit LSM303AGR Module (accelerometer + magnetometer) Use Seeeduino XIAO ESP32-S3 Required Libraries (install via Arduino Library Manager): - Adafruit_LSM303_Accel - Adafruit_LIS2MDL - Adafruit Unified Sensor - Adafruit BusIO - Adafruit GFX Library - Adafruit SSD1306 DroneBot Workshop 2025 https://dronebotworkshop.com */ // Include required libraries #include <Wire.h> #include <Adafruit_LSM303_Accel.h> #include <Adafruit_LIS2MDL.h> #include <Adafruit_Sensor.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C Adafruit_LSM303_Accel_Unified accel = Adafruit_LSM303_Accel_Unified(54321); Adafruit_LIS2MDL mag = Adafruit_LIS2MDL(12345); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); float mag_offset_x = 0.0; float mag_offset_y = 0.0; float mag_offset_z = 0.0; const int COMPASS_CENTER_X = 64; const int COMPASS_CENTER_Y = 40; const int COMPASS_RADIUS = 28; // Increased from 22 to 28 const int NEEDLE_LENGTH = 24; // Increased from 18 to 24 void setup() { Serial.begin(115200); delay(100); // Initialize I2C Wire.begin(); delay(100); Serial.println("Initializing OLED..."); if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println("SSD1306 failed"); while(1); } Serial.println("OLED OK"); display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 0); display.println("Initializing..."); display.display(); Serial.println("Initializing accel..."); if (!accel.begin()) { Serial.println("Accel failed!"); display.clearDisplay(); display.setCursor(0,0); display.println("Accel FAIL"); display.display(); while(1); } Serial.println("Accel OK"); Serial.println("Initializing mag..."); if (!mag.begin()) { Serial.println("Mag failed!"); display.clearDisplay(); display.setCursor(0,0); display.println("Mag FAIL"); display.display(); while(1); } Serial.println("Mag OK"); accel.setRange(LSM303_RANGE_4G); accel.setMode(LSM303_MODE_NORMAL); mag.setDataRate(LIS2MDL_RATE_100_HZ); display.clearDisplay(); display.setCursor(0, 0); display.println("Ready!"); display.println(""); display.println("Calibrate with"); display.println("figure-8 moves"); display.display(); delay(2000); } void loop() { sensors_event_t accel_event, mag_event; accel.getEvent(&accel_event); mag.getEvent(&mag_event); float mag_x = mag_event.magnetic.x - mag_offset_x; float mag_y = mag_event.magnetic.y - mag_offset_y; float heading = atan2(mag_y, mag_x) * 180.0 / PI; if (heading < 0) heading += 360.0; String direction = getCardinalDirection(heading); display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print("HDG: "); display.print(heading, 1); display.write(247); display.setCursor(0, 8); display.print("DIR: "); display.print(direction); display.drawCircle(COMPASS_CENTER_X, COMPASS_CENTER_Y, COMPASS_RADIUS, 1); drawCardinalMarkers(); drawCompassNeedle(heading); display.fillCircle(COMPASS_CENTER_X, COMPASS_CENTER_Y, 2, 1); display.display(); delay(100); } String getCardinalDirection(float heading) { const char* dirs[] = {"N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"}; return String(dirs[int((heading + 11.25) / 22.5) % 16]); } void drawCardinalMarkers() { // Cardinal direction labels - adjusted for larger circle display.setCursor(61, 4); // N - moved up slightly display.print("N"); display.setCursor(96, 36); // E - moved right display.print("E"); display.setCursor(61, 56); // S - moved down display.print("S"); display.setCursor(26, 36); // W - moved left display.print("W"); // Draw tick marks for intermediate directions for(int i=0; i<12; i++) { float a = i * 30.0 * PI / 180.0; display.drawLine(64+(COMPASS_RADIUS-3)*sin(a), 40-(COMPASS_RADIUS-3)*cos(a), 64+COMPASS_RADIUS*sin(a), 40-COMPASS_RADIUS*cos(a), 1); } } void drawCompassNeedle(float heading) { float a = heading * PI / 180.0; int nx = 64 + NEEDLE_LENGTH * sin(a); int ny = 40 - NEEDLE_LENGTH * cos(a); int sx = 64 - (NEEDLE_LENGTH*0.7) * sin(a); int sy = 40 + (NEEDLE_LENGTH*0.7) * cos(a); // Draw north needle (thicker) display.drawLine(64, 40, nx, ny, 1); display.drawLine(65, 40, nx+1, ny, 1); // Draw south needle display.drawLine(64, 40, sx, sy, 1); // Draw arrowhead on north end display.drawLine(nx, ny, nx-5*sin(a-2.8), ny+5*cos(a-2.8), 1); display.drawLine(nx, ny, nx-5*sin(a+2.8), ny+5*cos(a+2.8), 1); } |
Once again, remember to apply the offset values you obtained during calibration. You’ll see the three variables at the beginning of the code.
OLED Compass Demo
Load the code to your XIAO and reset the board. The display should prompt you to calibrate the compass by moving the sensor in a figure 8 pattern.
After that, the compass will display a circle with directional text.

Experiment by moving the compass around. Remember, in the arrangement I have with the solderless breadboard, the sensor is beside the display, so the movement will be erratic. In a permanent version, you would design it so the sensor was beneath the display and centered.
It’s a cute project, and with a larger display, it could be a practical instrument.
Conclusion
The LSM303AGR is a remarkably capable sensor that opens up numerous possibilities for motion-sensing and orientation-detection projects. By combining accelerometer and magnetometer data, you can create sophisticated applications that understand both linear movement and directional orientation.
The Adafruit module makes experimenting with this sensor very easy. Once you wrap your head around the fact that the LSM303AGR is really two distinct sensors in the same package, you’ll find that you can quickly create advanced projects, beyond a simple compass. Here are a few possibilities:
- Tilt-Compensated Compass: Combine accelerometer and magnetometer data for accurate heading at any orientation
- Motion-Activated Security: Detect movement and direction of approach
- Robotic Navigation: Provide orientation feedback for autonomous robots
- Camera Stabilization: Use accelerometer data for image stabilization algorithms
- Gesture Recognition: Interpret specific motion patterns as commands
- Structural Monitoring: Detect vibrations and orientation changes in structures
- Wearable Devices: Create fitness trackers or smart clothing with motion sensing
Whatever you decide to build with this, I hope this article will be a valuable resource in your work with the LSM303AGR.
Parts List
Here are some components that you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.
Adafruit LSM303AGR Adafruit Mouser
Resources
Code for this Article – All the code in one handy ZIP file
PDF Version – PDF Version of the article, packed in a ZIP file
MotionCal – The MotionCal calibration utility by Paul Stoffregen
LSM303AGR Datasheet – STMicroElectronics datasheet for LSM303AGR






Bill
The link to download the PDF of the article is broken. Please fix it when you get a chance.
Thanks,
Bob