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. | amazon | ||
3-axis Gyroscope Based on MPU6050 chip | amazon | ||
Breadboard Any size will do. | amazon | ||
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.
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.
To support developers in [[ countryRegion ]] I give a [[ localizedDiscount[couponCode] ]]% discount on all books and courses.
[[ activeDiscount.description ]] I'm giving a [[ activeDiscount.discount ]]% discount on all books and courses.
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(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.
>>> 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[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()
.
>>> 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 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.
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.