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 # pylint: disable=too-few-public-methods 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.
Single-Tap Detection
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
Double-Tap Detection
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:
Text editor powered by tinymce.