Home

Awesome

micropython-bno055

A MicroPython driver for the Bosch BNO055 inertial measurement unit (IMU). This chip has the advantage of performing sensor fusion in hardware. The driver is based on the Adafruit CircuitPython driver.

This driver has the following objectives:

Testing was done with the Adafruit BNO055 breakout. This chip and breakout come highly recommended. Calibration requires a little practice, but once done the fusion algorithm is remarkably immune to external magnetic fields. A field which displaced my hiker's compass by 90° caused at most 2° of heading change on this device. The raw magnetometer readings changed radically but heading remained essentially constant.

Contents

  1. Files and dependencies
  2. Getting started
    2.1 Pullups
    2.2 Clock stretching
    2.3 Pico issue
    2.4 Basic usage
  3. The BNO055 class
    3.1 Constructor
    3.2 Read only methods Read data from device.
    3.3 Changing the device configuration
    3.3.1 Mode setting Modify device operating mode.
    3.3.2 Rate and range control Further settings.
    3.4 Use in interrupt handlers
    3.5 Other methods
  4. Calibration
  5. Minimal version Minimise RAM usage.
  6. References

1. Files and dependencies

The driver has no dependencies. It is designed to be imported using

from bno055 import *

In addition to the BNO055 class this imports symbolic names for modes and data registers. On highly RAM-constrained targets the base class may be used alone with some loss of functionality, see section 5.

2. Getting started

The Adafruit breakout board has a voltage regulator and may be powered from a 5V or 3.3V supply. Note that other hardware may require a 3.3V source.

The wiring below is for I2C(1) as used in the test program.

pyboardbno055
VINVIN
GNDGND
SCL X9SCL
SDA X10SDA

2.1 Pullups

Pullups (resistors connected to 3.3V) are required on SCL and SDA. The Pyboard has these on I2C(1) and I2C(2), as does the Adafruit BNO055 breakout. ESP8266 boards have pullups on pins 0 and 2. Pyboard 1.1 pullups are 4.7KΩ, those on the Adafruit board are 10KΩ. The Raspberry Pico lacks pullups as do most ESP32 breakout boards.

I encountered problems with the Pico and the Adafruit board: waveforms had slow risetimes and invalid data occurred at times. This was solved by adding 1KΩ resistors, with waveforms showing clean edges.

As a general comment, my first port of call in the event of any problem would be to add 1KΩ resistors.

2.2 Clock stretching

See this issue.

The BNO055 hardware performs I2C clock stretching. I have found no documentation of this, but measurement suggests a maximum of about 500μs. The default timeout on Soft I2C is 255μs: it is therefore necessary to specify a timeout value in the SoftI2C constructor call - see below.

2.3 Pico Issue

Firmware problems with I2C on the Pico have now been fixed. Please use a daily build or release build later than 1.18. Running I2C at the 400KHz default can be unreliable if the I2C pullup resistors are too high. Options are to reduce speed (Adafruit use 100KHz) or to add 1KΩ resistors from SDA and SCL to 3.3V.

Refs: this forum thread and this issue.

2.4 Basic usage

import machine
import time
from bno055 import *
# Pyboard hardware I2C
i2c = machine.I2C(1)
# ESP32 and ESP8266 soft I2C
# i2c = machine.SoftI2C(scl=machine.Pin(2), sda=machine.Pin(0), timeout=100_000)
imu = BNO055(i2c)
calibrated = False
while True:
    time.sleep(1)
    if not calibrated:
        calibrated = imu.calibrated()
        print('Calibration required: sys {} gyro {} accel {} mag {}'.format(*imu.cal_status()))
    print('Temperature {}°C'.format(imu.temperature()))
    print('Mag       x {:5.0f}    y {:5.0f}     z {:5.0f}'.format(*imu.mag()))
    print('Gyro      x {:5.0f}    y {:5.0f}     z {:5.0f}'.format(*imu.gyro()))
    print('Accel     x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.accel()))
    print('Lin acc.  x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.lin_acc()))
    print('Gravity   x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.gravity()))
    print('Heading     {:4.0f} roll {:4.0f} pitch {:4.0f}'.format(*imu.euler()))

To calibrate the chip move the unit as per section 4 until calibration values for gyro, accel and mag are 3 and sys is >0.

Note that if code is started automatically on power up (by a line in main.py) a delay of 500ms should be applied before instantiating the BNO055. This is to allow for the BNO055 chip startup time (400ms typical).

Contents

3. The BNO055 class

3.1 Constructor

This takes the following args

The following optional args provide for vehicle relative coordinates. The default values assume that the IMU is mounted component side up and in the horizontal plane. Alternatives cater for cases where the IMU is rotated or mounted orthogonally relative to the horizontal plane of the vehicle.

Axes are numbered 0 for X, 1 for Y and 2 for Z, and the transpose tuple is (x, y, z). Hence (0, 1, 2) implies no transformation. Passing (1, 0, 2) implies a rotation around the Z axis.

Sign values must be 0 (normal) or 1 (inverted). Hence a board rotated around the Y axis and mounted upside down would have sign=(1, 0, 1) (X and Z axes inverted). This is further explained in the Device datasheet section 3.4.

The constructor blocks for 700ms (1.2s if crystal==True).

3.2 Read only methods

Return values (numbers are floats unless stated otherwise):

Some methods only produce valid data if the chip is in a fusion mode. If the mode is changed from the default to a non-fusion one, methods such as euler will return zeros. Such methods are marked with a * above.

See my notes on quaternions for code enabling them to be used to perform 3D rotation with minimal mathematics. They are easier to use for this purpose than Euler angles.

Contents

3.3 Changing the device configuration

Many applications will use the default mode of the chip. This section describes ways of changing this for special purposes, for example where a high update rate is required. This can arise if readings are used in a feedback loop where latency can cause stability issues.

3.3.1 Mode setting

The mode of operation defines which sensors are enabled, whether fusion occurs and if measurements are absolute or relative. The bno055 module provides the mode values listed below as integers.

ModeAccelCompassGyroAbsoluteFusion mode
CONFIG_MODE----N
ACCONLY_MODEX---N
MAGONLY_MODE-X--N
GYRONLY_MODE--X-N
ACCMAG_MODEXX--N
ACCGYRO_MODEX-X-N
MAGGYRO_MODE-XX-N
AMG_MODEXXX-N
IMUPLUS_MODEX-X-Y
COMPASS_MODEXX-XY
M4G_MODEXX--Y
NDOF_FMC_OFF_MODEXXXXY
NDOF_MODEXXXXY

The default mode is NDOF_MODE which supports fusion with absolute heading. This example illustrates restoration of the prior mode (imu is a BNO055 instance):

from bno055 import *
# code omitted to instantiate imu
old_mode = imu.mode(ACCGYRO_MODE)
# code omitted
imu.mode(old_mode)

The purpose of the various modes is covered in the Device datasheet section 3.3.

Contents

3.3.2 Rate and range control

In non-fusion modes the chip allows control of the update rate of each sensor and the range of the accelerometer and gyro. In fusion modes rates are fixed: the only available change is to the accelerometer range. The folowing shows the default setings after initialisation (mode is NDOF). The update rate of fusion values is 100Hz.

DeviceFull scaleUpdate rate
Accel+-4G100Hz
Gyro2000°/s100Hz
Mag-20Hz

The magnetometer has a single range: units are Micro Tesla (μT).

In modes which permit it, sensors may be controlled with the following method.

The default value of None causes no change to be made. In each case the method returns the config tuple as it was before any change was made. In certain circumstances the chip can return an unknown value. This was observed in the case of the initial value from the magnetometer. In such cases the result will be False. Returning the prior value allows old values to be restored, e.g.

old_config = imu.config(ACC, (2, 250))
# ...
if old_config:  # Valid config returned
    imu.config(ACC, old_config)  # Restore old config

Note that the hardware will only allow configuration changes in appropriate modes. For example to change gyro settings the chip must be in a non-fusion mode which enables the gyro. If the mode is such that changes are not allowed, failure will be silent. If in doubt check the result by reading back the resultant config:

imu.mode(ACCGYRO_MODE)  # Allows config change to ACC or GYRO
cfg = (2, 250)  # Intended config
imu.config(ACC, cfg)
if imu.config(ACC) == cfg:
    print('Success')

Accelerometer (dev == ACC)

value is a 2-tuple comprising (range, bandwidth)
Allowable values:
Range: 2, 4, 8, 16 (G).
Bandwidth: 8, 16, 31, 62, 125, 250, 500, 1000 (Hz).
The outcome of a change may be shown by means of the .config(ACC) method.

from bno055 import *
# code omitted
imu.mode(ACCONLY_MODE)  # non-fusion mode
cfg = (2, 1000)  # Intended config
imu.config(ACC, cfg) # Update.
if imu.config(ACC) == cfg:
    print('Success')

Gyro (dev == GYRO)

value is a 2-tuple comprising (range, bandwidth)
Allowable values:
Range: 125, 250, 500, 1000, 2000 (dps)
Bandwidth: 12, 23, 32, 47, 64, 116, 230, 523 (Hz).
The outcome of a change may be shown by means of the .config(GYRO) method.

Magnetometer (dev == MAG)

value is a 1-tuple comprising (rate,) being the update rate in Hz.
Allowable values:
Rate: 2, 6, 8, 10, 15, 20, 25, 30 (Hz)
The outcome of a change may be shown by means of the .config(MAG) method. Note that on first call the prior config may be unknown and the method will return False. This is a chip behaviour.

Contents

3.4 Use in interrupt handlers

The BNO055 class supports access in interrupt service routines (ISR's) by means of the iget method and w, x, y, and z bound variables. The ISR calls iget with the name of the value to be accessed. On return the bound variables are updated with the raw data from the device. Each value is a signed integer that requires scaling to be converted to standard units of measurement.

NameScalingUnits
ACC_DATA1/100m.s^-2
MAG_DATA1/16μT
GYRO_DATA1/16°.s^-1
GRAV_DATA1/100m.s^-2
LIN_ACC_DATA1/100m.s^-2
EULER_DATA1/16°
QUAT_DATA1/(1 << 14)unitless

In each case the integer values must be multiplied by the scaling to give the units specified. In all cases other than quaternion (QUAT_DATA) the iget method sets .w to zero. Example usage:

def cb(t):
    imu.iget(ACC_DATA)
    print(imu.w, imu.x, imu.y, imu.z)

t = pyb.Timer(1, period=200, callback=cb)

3.5 Other methods

Contents

4. Calibration

Calibration requires only movement of the device while running: the process is internal to the chip and its nature is opaque. The calibration status may be read by methods described in section 3.2.

Until the device is calibrated its orientation will be relative to that when it was powered on. When system calibration status becomes 1 or higher the device has found magnetic north and orientation values become absolute. (Source). The status returned by the chip, and hence the return values of .calibrated and .cal_status methods can regress after successful calibration. The meaning of this is unclear. It seems reasonable to assume that once the chip returns a good status it can be assumed to be OK; the demo scripts make that assumption.

The following text is adapted from the chip datasheet; it could be clearer. I recommend watching this Bosch video for a good exposition.

Though the sensor fusion software runs the calibration algorithm of all the three sensors (accelerometer, gyroscope and magnetometer) in the background to remove the offsets, some preliminary steps should be ensured for this automatic calibration to take place.

The accelerometer and the gyroscope are relatively less susceptible to external disturbances, as a result of which the offset is negligible. Whereas the magnetometer is susceptible to external magnetic field and therefore to ensure proper heading accuracy, the calibration steps described below have to be taken.

Depending on the sensors selected, the following simple steps should be taken after every ‘Power on Reset’ for proper calibration of the device.

Accelerometer Calibration

Gyroscope Calibration

Magnetometer Calibration

Magnetometers in general are susceptible to both hard-iron and soft-iron distortions, but the majority of the cases are rather due to the former. The steps mentioned below are to calibrate the magnetometer for hard-iron distortions.

NDOF:

Calibration Restoration

Restoring previous calibration offsets after a reset is also supported, via the sensor_offsets() and set_offsets() methods.

After a successful calibration, sensor_offsets() can be called to retrieve a bytearray. This can then, for example, be written to disk for loading after a reset of the device.

The corresponding set_offsets() method allows for restoring calibration offsets stored in said bytearray. Please be aware that the magnetometer's calibration status will remain as 0, as per the cal_status() method, even after restoring its offsets.

Contents

5. Minimal Version

This is intended for devices such as ESP8266 where RAM is limited. Note that the full version will run on ESP8266 using ~14K of RAM. The minimal version reduces this to just over 9K.

The minimal version does not support vehicle-relative coordinates, ISR usage or configuration changes. Mode changes can be done, but symbolic names of modes are not supplied. The version is primarily intended for use in the default NDOF mode.

In use the bno055_base module is imported and the base class is used. Example tested on an ESP8266:

import machine
import time
from bno055_base import BNO055_BASE

i2c = machine.I2C(-1, scl=machine.Pin(2), sda=machine.Pin(0))
imu = BNO055_BASE(i2c)
calibrated = False
while True:
    time.sleep(1)
    if not calibrated:
        calibrated = imu.calibrated()
        print('Calibration required: sys {} gyro {} accel {} mag {}'.format(*imu.cal_status()))
    print('Temperature {}°C'.format(imu.temperature()))
    print('Mag       x {:5.0f}    y {:5.0f}     z {:5.0f}'.format(*imu.mag()))
    print('Gyro      x {:5.0f}    y {:5.0f}     z {:5.0f}'.format(*imu.gyro()))
    print('Accel     x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.accel()))
    print('Lin acc.  x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.lin_acc()))
    print('Gravity   x {:5.1f}    y {:5.1f}     z {:5.1f}'.format(*imu.gravity()))
    print('Heading     {:4.0f} roll {:4.0f} pitch {:4.0f}'.format(*imu.euler()))

6. References

Adafruit BNO055 breakout
Adafruit CircuitPython driver.
Device datasheet