Overview
This page of notes by CGrover was used to develop BNO055 9-DoF sensor algorithms for the PowerWash Simulator Controller project and discusses three essential characteristics of the sensor.
First, the relative and absolute calibration of the sensor can be performed to improve initial sensor stability and positioning. Stand-alone sensor calibrator code is shown and was submitted to the driver library's examples folder.
Next we'll talk about how to measure and adjust for user position orientation without changing the sensor's absolute position calibration.
Finally, since tap detection is not native to the BNO055 chip, an example of how to detect single and double-taps with the accelerometer component of the sensor is described.
BNo055 Sensor Calibration
The sensor's offset registers each contain (0, 0, 0) after power-on, indicating that one or more of the sensor components isn't fully calibrated. If left alone, the sensor would eventually calibrate as its internal background calibration routine watches the sensor's movement. It can take a long time to calibrate the sensor in this manner unless the user executes a calibration dance just after power-up.
The preferred approach is to conduct a calibration dance using a separate module that provides offset values that can be inserted into the project code to preset the sensor's offset registers just after power-up. Presetting the magnetometer is important since reliable absolute positioning is dependent on the magnetometer knowing its geographic location relative to magnetic north. Presetting the gyroscope and accelerometer registers isn't as critical, but is a good practice.
Here's the stand-alone calibrator method that's included in the driver library's examples folder:
# SPDX-FileCopyrightText: 2023 JG for Cedar Grove Maker Studios
# SPDX-License-Identifier: MIT
"""
`bno055_calibrator.py`
===============================================================================
A CircuitPython module for calibrating the BNo055 9-DoF sensor. After manually
calibrating the sensor, the module produces calibration offset tuples for use
in project code.
* Author(s): JG for Cedar Grove Maker Studios
Implementation Notes
--------------------
**Hardware:**
* Adafruit BNo055 9-DoF sensor
**Software and Dependencies:**
* Driver library for the sensor in the Adafruit CircuitPython Library Bundle
* Adafruit CircuitPython firmware for the supported boards:
https://circuitpython.org/downloads
"""
import time
import board
import adafruit_bno055
class Mode:
CONFIG_MODE = 0x00
ACCONLY_MODE = 0x01
MAGONLY_MODE = 0x02
GYRONLY_MODE = 0x03
ACCMAG_MODE = 0x04
ACCGYRO_MODE = 0x05
MAGGYRO_MODE = 0x06
AMG_MODE = 0x07
IMUPLUS_MODE = 0x08
COMPASS_MODE = 0x09
M4G_MODE = 0x0A
NDOF_FMC_OFF_MODE = 0x0B
NDOF_MODE = 0x0C
# Uncomment these lines for UART interface connection
# uart = board.UART()
# sensor = adafruit_bno055.BNO055_UART(uart)
# Instantiate I2C interface connection
# i2c = board.I2C() # For board.SCL and board.SDA
i2c = board.STEMMA_I2C() # For the built-in STEMMA QT connection
sensor = adafruit_bno055.BNO055_I2C(i2c)
sensor.mode = Mode.NDOF_MODE # Set the sensor to NDOF_MODE
print("Magnetometer: Perform the figure-eight calibration dance.")
while not sensor.calibration_status[3] == 3:
# Calibration Dance Step One: Magnetometer
# Move sensor away from magnetic interference or shields
# Perform the figure-eight until calibrated
print(f"Mag Calib Status: {100 / 3 * sensor.calibration_status[3]:3.0f}%")
time.sleep(1)
print("... CALIBRATED")
time.sleep(1)
print("Accelerometer: Perform the six-step calibration dance.")
while not sensor.calibration_status[2] == 3:
# Calibration Dance Step Two: Accelerometer
# Place sensor board into six stable positions for a few seconds each:
# 1) x-axis right, y-axis up, z-axis away
# 2) x-axis up, y-axis left, z-axis away
# 3) x-axis left, y-axis down, z-axis away
# 4) x-axis down, y-axis right, z-axis away
# 5) x-axis left, y-axis right, z-axis up
# 6) x-axis right, y-axis left, z-axis down
# Repeat the steps until calibrated
print(f"Accel Calib Status: {100 / 3 * sensor.calibration_status[2]:3.0f}%")
time.sleep(1)
print("... CALIBRATED")
time.sleep(1)
print("Gyroscope: Perform the hold-in-place calibration dance.")
while not sensor.calibration_status[1] == 3:
# Calibration Dance Step Three: Gyroscope
# Place sensor in any stable position for a few seconds
# (Accelerometer calibration may also calibrate the gyro)
print(f"Gyro Calib Status: {100 / 3 * sensor.calibration_status[1]:3.0f}%")
time.sleep(1)
print("... CALIBRATED")
time.sleep(1)
print("\nCALIBRATION COMPLETED")
print("Insert these preset offset values into project code:")
print(f" Offsets_Magnetometer: {sensor.offsets_magnetometer}")
print(f" Offsets_Gyroscope: {sensor.offsets_gyroscope}")
print(f" Offsets_Accelerometer: {sensor.offsets_accelerometer}")
Dance Step One: The Figure-Eight
- To calibrate the magnetometer, wave the sensor slowly in a figure-8 pattern until the REPL says "CALIBRATED."
Dance Step Two: The Six-Step Rotate
- The accelerometer is then calibrated by holding it on an edge facing you for a few seconds then rotating it clockwise 90 degrees, wait, and repeat for a total of 4 positions. Then place it face-up on a flat surface and hold it there for a few seconds. Finally, flip it face down and hold it to complete the accelerometer calibration.
Dance Step Three: The Look Up and Wait
- The last step is for the gyroscope. All it needs is to be held still for a few seconds in a face-up position. The accelerometer calibration usually takes care of the gyroscope calibration.
After all three calibration dances complete, the preset offset values will appear in the REPL.
Repeating the calibration process produces some variance in the offset values, but the scale and magnitude are usually close. Since the sensor is continuously calibrating, close is good enough for most projects. The primary benefit of calibrating the sensor once using the stand-alone code is that the project application begins with a useful orientation from the get-go and won't require a calibration dance recital for each power-on startup.
Optional: Preserving Calibration between Power On/Off Cycles
Rather than the copy/paste method described above, storing and reusing configuration offsets from one power-on/off session to the next is possible since the offset register properties can be read and changed. After conducting a single stand-alone calibration, store the calibration offset registers into the NVM memory or an SD card file for use during subsequent power-on startups. You may also want to consider updating the NVM or file periodically during regular use as the offset registers are continually adjusted by the internal background calibration task.
User Orientation Offset (Target Angle Offset)
A user orientation offset to correct for the alignment of the display in relationship with the sensor will usually be needed by a project, initiated with a button press or other event like an accelerometer double-tap. Changing the target angle offset doesn't recalibrate the sensor, it just uses the current Euler angle to provide the offset for future position readings.
# The target angle offset used to reorient the sensor
# (heading, roll, pitch)
target_angle_offset = (0, 0, 0)
# The project's main while loop
while True:
# Get the Euler angle values from the sensor
# The Euler angle limits are: +180 to -180 pitch, +360 to -360 heading, +90 to -90 roll
sensor_euler = sensor.euler
print(f"Euler angle: {sensor_euler}")
# Adjust the Euler angle values with the target_angle_offset
heading, roll, pitch = [position - target_angle_offset[idx] for idx, position in enumerate(sensor_euler)]
# Scale the heading for horizontal movement range
horizontal_mov = int(map_range(heading, -20, 20, -30, 30))
print(f"mouse x: {horizontal_mov}")
# Scale the roll for vertical movement range
vertical_mov = int(map_range(roll, -25, 25, 30, 30))
print(f"mouse y: {vertical_mov}")
# Translate to stuff needed for HID
mouse.move(x=horizontal_mov)
mouse.move(y=vertical_mov)
# Check the "reorient" button was pressed
if reorientation_button:
print(f"Reorient the sensor")
# Use the current Euler angle values to reorient the target angle
target_angle_offset = [angle for angle in sensor_euler]
Tap Detection
Here's a fairly simple non-blocking single and double tap detection scheme that takes advantage of the BNO055's 100Hz-ish measurement data rate. The accelerometer's data rate acts like a high pass filter when measuring the delta between two measurements.
The tap sensitivity threshold can be set to accommodate the sensor's mechanical mounting scheme; 1.0 is overly sensitive, 5.0 is typical, and 10 is somewhat numb. The tap debounce setting can also vary somewhat depending on the sensor mount; 0.1 seconds works for nicely for sensors that are securely attached, 0.3 is typical if the sensor is suspended in foam, and 0.5 may be needed if mounted loosely. Adjust these values for your project's particulars.
def euclidean_distance(reference, measured):
"""Calculate the Euclidean distance between reference and measured points
in a universe. The point position tuples can be colors, compass,
accelerometer, absolute position, or almost any other multiple value data
set.
reference: A tuple or list of reference point position values.
measured: A tuple or list of measured point position values."""
# Create list of deltas using list comprehension
deltas = [(reference[idx] - count) for idx, count in enumerate(measured)]
# Resolve squared deltas to a Euclidean difference and return the result
return math.sqrt(sum([d ** 2 for d in deltas]))
# Set the tap detector parameters
TAP_THRESHOLD = 6 # Tap sensitivity threshold; depends on the physical sensor mount
TAP_DEBOUNCE = 0.3 # Time for accelerometer to settle after tap (seconds)
# The project's main while loop
while True:
# Detect a single tap on any axis of the BNo055 accelerometer
accel_sample_1 = sensor.acceleration # Read one sample
accel_sample_2 = sensor.acceleration # Read the next sample
if euclidean_distance(accel_sample_1, accel_sample_2) >= TAP_THRESHOLD:
# The difference between two consecutive samples exceeded the threshold
# (equivalent to a high-pass filter)
print(f"SINGLE tap detected")
#
# Perform the single-tap task here
#
time.sleep(TAP_DEBOUNCE) # Debounce delay
def euclidean_distance(reference, measured):
"""Calculate the Euclidean distance between reference and measured points
in a universe. The point position tuples can be colors, compass,
accelerometer, absolute position, or almost any other multiple value data
set.
reference: A tuple or list of reference point position values.
measured: A tuple or list of measured point position values."""
# Create list of deltas using list comprehension
deltas = [(reference[idx] - count) for idx, count in enumerate(measured)]
# Resolve squared deltas to a Euclidean difference and return the result
return math.sqrt(sum([d ** 2 for d in deltas]))
# Set the BNo055 tap detector parameters and initialize tap event history list
TAP_THRESHOLD = 6 # Tap sensitivity threshold; depends on the physical sensor mount
TAP_DEBOUNCE = 0.1 # Time for accelerometer to settle after tap (seconds)
TAP_TIMEOUT = 1500 # Remove tap event from history timeout (milliseconds)
tap_events = [] # Initialize the tap event history list
# The project's main while looop
while True:
# Detect a tap on any axis of the BNo055 accelerometer
accel_sample_1 = sensor.acceleration # Read one sample
accel_sample_2 = sensor.acceleration # Read the next sample
if euclidean_distance(accel_sample_1, accel_sample_2) >= TAP_THRESHOLD:
# The difference between two consecutive samples exceeded the threshold
# (equivalent to a high-pass filter)
print(f"SINGLE tap detected {ticks_ms()}")
tap_events.append(ticks_ms() + TAP_TIMEOUT) # save tap expiration time in event stack
time.sleep(TAP_DEBOUNCE) # Debounce delay
# Clean up tap event history after timeout period expires
if len(tap_events) > 0:
# Check for expired events
if tap_events[0] <= ticks_ms():
# The oldest event has expired
tap_events = tap_events[1:] # Remove the oldest event
# Check see if two taps are in the event history list
if len(tap_events) == 2:
# Double-tap: execute the task and clear event history
print(f"DOUBLE tap detected {ticks_ms()}")
#
# Perform the double-tap task here
#
tap_events = [] # Clear event history
Additional Information
A very good calibration reference by MathWorks, but the axis orientation doesn't represent the default setting: https://www.mathworks.com/help/supportpkg/arduinoio/ug/calibrate-sensors.html
Bosch:
https://www.youtube.com/watch?v=Bw0WuAyGsnY
BNO055 Sensor CircuitPython Driver GitHub:
https://github.com/adafruit/Adafruit_CircuitPython_BNO055
BNO055 Sensor ReadTheDocs:
Page last edited January 22, 2025
Text editor powered by tinymce.