3-axis Accelerometer-Gyro
Measuring acceleration and orientation with an MPU6050

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 Specificiation.

Requirements
Wemos D1 v2.2+ or good imitations. Buy
3-axis Gyroscope Based on MPU6050 chip Buy
Breadboard Any size will do. Buy
Wires Loose ends, or jumper leads.

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.

You can download the mpu6050.py file directly. Click Raw format and save the file with a .py extension. Upload the file to your device using ampy tool (or the WebREPL):

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:

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

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.

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.

>>> 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.

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(samples):
        data = accel.get_values()

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

    return result

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

>>> 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.47g 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.

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[key] - v2[key]) < threshold for key in v1.keys()):
            return v1  # Calibrated.

The all(abs(v1[key] - v2[key]) < 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[key] - v2[key]) 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().

>>> 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.

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 key in data.keys():
            # Add on value / n_samples to produce an average
            # over n_samples, with default of 0 for first loop.
            result[m] = result.get(m, 0) + (data[m] / samples)

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

    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.

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.

Continue reading

Gyroscopic 3D wireframe cube

This little project combines the previous accelerometer-gyroscope code with the 3D rotating OLED cube to produce a 3D cube which responds to gyro input, making it possible to "peek around" the cube with simulated perspective, or make it spin with a flick of the wrist. Take a look at those … More