> ## Content Index
> Fetch the complete content index at: https://mfitzp.ghost.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# 3-axis Accelerometer-Gyro with MicroPython
- URL: https://mfitzp.ghost.io/3-axis-gyro-micropython/
- Published: 2019-01-01T00:00:00.000Z
- Updated: 2026-09-09T13:28:23.000Z
- Description: Measuring acceleration and orientation with an MPU6050
- Author: Martin Fitzpatrick
- Tags: Tutorials, MicroPython, Python, #Import 2024-03-14 08:20, #Import 2026-08-24 14:26

Measuring acceleration and rotation has a lot of useful applications, from drone or rocket stablisation to making physically interactive handheld games.

An accelerometer measures *proper* acceleration, meaning the rate of change of velocity relative to it's own *rest frame*. This is in contrast to *coordinate* acceleration, which is relative to a fixed coordinate system. The practical upshot of this is that at rest on Earth an accelerometer will measure acceleration due to the Earth's gravity, of *g* ≈ 9.81 m/s. An accelerometer in freefall will measure zero. This can be adjusted for with calibration.

A gyroscope (from Ancient Greek γῦρος "circle" and σκοπέω "to look") in contrast measures orientation and *angular* velocity, or rotation around an an axis. Angular velocity will always be zero at rest.

The availability of cheap single-chip accelerometer-gyroscope packages makes them practical for any project.

## MPU6050

The MPU6050 is a nifty little 3-axis accelerometer and gyro package, providing measurements for acceleration along and rotation around 3 axes. It also contains an inbuilt temperature sensor. There are 4 configurable ranges for the gyro and accelerometer, meaning it can be used for both micro and macro measurements. Communication is via a simple I2C interface.

| Gyro Full Scale Range (°/sec) | Gyro Sensitivity (LSB/°/sec) | Gyro Rate Noise (dps/√Hz) | Accel Full Scale Range (g) | Accel Sensitivity (LSB/g) |
| ----------------------------- | ---------------------------- | ------------------------- | -------------------------- | ------------------------- |
| ±250                          | 131                          | 0.005                     | ±2                         | 16384                     |
| ±500                          | 65.5                         | 0.005                     | ±4                         | 8192                      |
| ±1000                         | 32.8                         | 0.005                     | ±8                         | 4096                      |
| ±2000                         | 16.4                         | 0.005                     | ±16                        | 2048                      |

See the [full MPU-6050 Product Specification](https://www.invensense.com/products/motion-tracking/6-axis/mpu-6050/?ref=mfitzp.ghost.io). 

## Requirements

- Wemos D1 v2.2+ or good imitations.
- 3-axis Gyroscope (this tutorial is based on the MPU6050 chip)
- Breadboard
- Wires

## Setting up

The MPU-6050 provides an I2C interface for communication. There are Python libraries available which simplify the communication further and return the measurements in a simple format. The examples here are using [this MPU6050 library](https://github.com/adamjezek98/MPU6050-ESP8266-MicroPython?ref=mfitzp.ghost.io).

You can download the [mpu6050.py](https://github.com/adamjezek98/MPU6050-ESP8266-MicroPython/blob/master/mpu6050.py?ref=mfitzp.ghost.io) file directly. Click *Raw* format and save the file with a `.py` extension. Upload the file to your device using [ampy](https://github.com/adafruit/ampy?ref=mfitzp.ghost.io) tool (or the WebREPL):

```Bash
ampy --port /dev/tty.wchusbserial141120 put mpu6050.py

```

With the `mpu6050.py` file on your Wemos D1, you can import it as any other Python module. Connect to your device, and then in the REPL enter:

```Python
from machine import I2C, Pin
import mpu6050

```

If the `import mpu6050` succeeds, the package is correctly uploaded and you're good to go.

![Wiring for 3-axis Gyro with Wemos D1](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/gyro-wemos-d1.jpg)

Wire up the MPU6050, connecting pins `D1` to `SCL` and `D2` to `SDA`. Provide power from `G` and `5V`. The light on the MPU6050 should light up once it's active.

## Reading values

With the `mpu6050` Python library on your device, and the MPU6050 module wired up, you can connect to the shell and start talking to it.

```Python
from machine import I2C, Pin
import mpu6050

i2c = I2C(scl=Pin(5), sda=Pin(4))
accel = mpu6050.accel(i2c)

```

With the `accel` object, we can read values from the sensor with `.get_values()`. This will return a dictionary of measurements from the sensor.

```Python
>>> accel.get_values()
{'GyZ': -46, 'GyY': -135, 'GyX': -1942, 'Tmp': 26.7888, 'AcZ': 24144, 'AcY': 68, 'AcX': -1004}

```

There are 3 sets of measurements returned from the sensor — *acceleration*, *rotation* (gyration) and *temperature*. The *acceleration* and *rotation* measurements provide 3 values each, one for each of the axes (X, Y, Z).

| Measurement | Description                 |
| ----------- | --------------------------- |
| AcX         | Acceleration *along* X axis |
| AcY         | Acceleration *along* Y axis |
| AcZ         | Acceleration *along* Z axis |
| GyX         | Rotation *around* X axis    |
| GyY         | Rotation *around* Y axis    |
| GyZ         | Rotation *around* Z axis    |
| Tmp         | Temperature °C              |

🧠

The direction of the X and Y axes relative to the sensor are shown on the module itself. But you can always just adjust your code by trial and error.

## Smoothing

If you repeatedly read measurements from the sensor in this way you'll notice that they're bouncing all over the place. This is normal for analog sensors. Before we can use the measurements from the sensor, we need to *smooth out* these random fluctuations to leave us with real representative data.

A simple way to do this is to read multiple values and take the mean (or median) of all the values. The sensor returns multiple values, so we need to average all of these individually.

```Python
def get_smoothed_values(n_samples=10):
    """
    Get smoothed values from the sensor by sampling
    the sensor `n_samples` times and returning the mean.
    """
    result = {}
    for _ in range(n_samples):
        data = accel.get_values()

        for k in data.keys():
            # Add on value / n_samples (to generate an average)
            # with default of 0 for first loop.
            result[k] = result.get(k, 0) + (data[k] / n_samples)

    return result

```

Running the above function will sample the sensor ten times, and return the averaged values.

```Python
>>> accel.get_values()
{'GyZ': -46, 'GyY': -135, 'GyX': -1942, 'Tmp': 26.7888, 'AcZ': 24144, 'AcY': 68, 'AcX': -1004}

```

If you repeatedly run this you should find the samples have stabilised quite a bit. As the values are in the range ±32768 the above measurements are actually *pretty close* to zero, though *Z acceleration* is notably higher. `AcZ` is the acceleration measurement in the Z (straight-up) axis and this value is the acceleration due to gravity of *g* ≈ 9.81 m/s.

The measured value 24144/16384 (at lowest measurement sensitivity) gives 1.47*g* so it's still off a bit.

The other offsets at rest are just inherent errors in the chip (individual, not the model), they're not interesting. To get our measurements centred around zero we can must identify this bias and adjust for it through *calibration*.

## Calibration

If we take a number of repeated sensor measurements over time we can determine the standard, or average, deviation from zero over time. This offset can then be subtracted from future measurements to correct them. The device must be at rest and not changing for this to work reliably.

```Python
def calibrate(threshold=50, n_samples=100):
    """
    Get calibration date for the sensor, by repeatedly measuring
    while the sensor is stable. The resulting calibration
    dictionary contains offsets for this sensor in its
    current position.
    """
    while True:
        v1 = get_accel(n_samples)
        v2 = get_accel(n_samples)
        # Check all consecutive measurements are within
        # the threshold. We use abs() so all calculated
        # differences are positive.
        if all(abs(v1[k] - v2[k]) < threshold for k in v1.keys()):
            return v1  # Calibrated.

```

The `all(abs(v1[k] - v2[k]) < threshold for key in v1.keys())` line is a bit of a beast. It iterates all the keys in our `v1` dictionary, testing `abs(v1[k] - v2[k])` for each. Here `abs()` gives us the *absolute* or positive difference, so we don't need to compare against negative `threshold`. Finally, `all()` tests that this is true for every key we've iterated.

Run this `calibrate()` function and wiggle the sensor around. You will see the device remain in the calibrating state, with the light flashing, while you wiggle it.

This is because while the device is moving, the difference between consecutive measurements will be *greater* than the defined threshold.

In the above calibration method we're testing all measurements from the sensor. You could of course only test some of them — e.g. only gyro or acceleration — depending on what you're using.

If you place your sensor onto the table, the calibration test will pass and the function will return values in the same format as for `.get_values()`.

```Python
>>> calibrate()
{'GyZ': -46, 'GyY': -115, 'GyX': -1937, 'Tmp': 26.8359, 'AcZ': 23960, 'AcY': 44, 'AcX': -872}

```

The output dictionary of base measurements can be used to adjust subsequent measurements to remove this offset and recalibrate to zero at rest.

Below is an updated `get_smoothed_values` function which removes the calibrated offset before returning the smoothed data.

```Python
def get_smoothed_values(n_samples=10, calibration=None):
    """
    Get smoothed values from the sensor by sampling
    the sensor `n_samples` times and returning the mean.

    If passed a `calibration` dictionary, subtract these
    values from the final sensor value before returning.
    """
    result = {}
    for _ in range(n_samples):
        data = accel.get_values()

        for k in data.keys():
            # Add on value / n_samples to produce an average
            # over n_samples, with default of 0 for first loop.
            result[k] = result.get(k, 0) + (data[k] / n_samples)

    if calibration:
        # Remove calibration adjustment.
        for k in calibration.keys():
            result[k] -= calibration[k]

    return result

```

The following short snippet will allow you to see a table of the gyro and acceleration measurements in (very smoothed) real-time. The numbers are padded to stop them bouncing around as they change, and it uses control-characters to clear the terminal.

```Python
calibration = calibrate()
while True:
    data = get_smoothed_values(n_samples=100, calibration=calibration)
    print(
        '\t'.join('{0}:{1:>10.1f}'.format(k, data[k])
        for k in sorted(data.keys())),
    end='\r')

```

Running this you should see something like the following at rest:

```
AcX:     -17.7  AcY:      -3.2  AcZ:      -4.2  GyX:      -1.3  GyY:       1.9  GyZ:       1.8  Tmp:       0.0

```

If you pick up the sensor, you should see the `Z` acceleration increase, or decrease as you drop it. The `X` and `Y` acceleration should increase/decrease if you tilt the device in any direction. The acceleration measured here is acceleration due to gravity — if you tilt the device so the X axis is pointing straight down, all of *g* (≈ 9.81 m/s) will be acting through X, and none through `Z`.

```
AcX:   17679.9  AcY:     233.8  AcZ:  -16332.6  GyX:       3.9  GyY:      32.5  GyZ:       1.0  Tmp:      -0.2

```

Gyroscopic measurements show rotation around the relevant axis, so will always be zero at rest, but increase with rotational speed in each axis. For example if you rotate the device away from you, you should see a spike in the `Y` gyroscopic value, which returns to zero as the unit comes to a rest.

```
AcX:   -6540.4  AcY:      22.4  AcZ:   -1930.1  GyX:    -116.7  GyY:     748.9  GyZ:     211.3  Tmp:       0.0

```

If you pop your finger on the chip, you should also see the temperature raise very slightly.

```
AcX:    -547.8  AcY:      29.8  AcZ:     -36.0  GyX:      -6.9  GyY:       8.9  GyZ:      -3.8  Tmp:       0.3

```

This covers the basic work of interfacing with an MPU6050 from MicroPython. I'll be adding some projects using this chip shortly.