BNO08x – 9-DoF-IMUs

Über den Beitrag

In diesem Beitrag gehe ich auf die Eigenschaften der BNO08x-Module ein und zeige, wie ihr sie mit gängigen Bibliotheken nutzt. Die BNO08x-Module beinhalten einen 3-Achsen-Beschleunigungssensor (Accelerometer), ein 3-Achsen-Gyroskop und einen 3-Achsen-Magnetfeldsensor. Solche Multitalente habe ich schon zuvor in meinen Beiträgen über den ICM-20948 und den MPU9250 beschrieben. Dennoch lohnt sich ein Blick auf die BNO08x-Module: Sie liefern nicht nur die Sensordaten, sondern berechnen daraus auch die absolute räumliche Ausrichtung und erkennen darüber hinaus komplexe Bewegungsmuster wie Schritte, Schüttelbewegungen oder Antippen.

Den kleinen Bruder der BNO08x-Module, den BNO055, werde ich in einem separaten Beitrag besprechen.

Folgendes erwartet euch:

BNO08x im Überblick

Zu der BNO08X-Familie gehören laut Datenblatt der BNO085 und der BNO086. Ihr Vorgänger, der BNO080 (Link zum Datenblatt), wird nicht mehr hergestellt. Trotzdem könntet ihr noch auf ihn stoßen. Die gute Nachricht ist, dass BNO080, BNO085 und BNO086 weitestgehend kompatibel sind. 

Alle drei vereint, dass es sich um „9-DoF-IMUs“ handelt:

  • 9-DoF = 9 Degrees of Freedom (neun Freiheitsgrade). Je drei Achsen für den Beschleunigungssensor, das Gyroskop und den Magnetsensor.
  • IMU = Inertial Measurement Unit, also auf Trägheit (lateinisch: Inertia) basierende Sensoren. Das Messprinzip habe ich hier in meinem Beitrag über den MPU6050 beschrieben.  

Die Umrechnung der Sensordaten in die absolute Ausrichtung im Raum und die Erkennung von Bewegungsmustern nennt man Sensorfusion. Da die Sensorfusion sehr rechenintensiv ist, besitzen die BNO08x-Sensoren einen eigenen Mikrocontroller (ARM Cortex-M0+), der diese Aufgabe übernimmt und euren angeschlossenen Mikrocontroller entlastet.

Auf dem internen Mikrocontroller läuft die SH-2-Software der Firma CEVA. Ursprünglich entwickelt wurde SH-2 von Hillcrest Labs, die später von CEVA übernommen wurden.

Die eigentlichen Beschleunigungs-, Gyroskop- und Magnetsensoren stammen dabei von Bosch Sensortec.

Wenn ihr Sensoren über I²C, SPI oder UART ansteuert oder auslest, erfolgt der Zugriff normalerweise direkt über Register der jeweiligen Hardware. Beim BNO08x ist das anders: Hier greift ihr nicht direkt auf die Sensorhardware zu. Stattdessen bildet die SH-2-Software die Schnittstelle zwischen eurem Mikrocontroller und den internen Sensoren. Möchtet ihr Daten vom BNO08x erhalten, fragt ihr diese über sogenannte Report-IDs an. Jede Report-ID steht dabei für einen bestimmten Datentyp oder eine bestimmte Berechnung, wie etwa Beschleunigungswerte, Euler-Winkel oder den Schrittzähler.

Viel Leistung – hohe Ansprüche

Die hohe Leistungsfähigkeit der BNO08x-Module hat jedoch ihren Preis. Auf klassischen Arduino-Boards mit ATmega328P lassen sich die von mir besprochenen Bibliotheken von SparkFun und Adafruit nicht einsetzen, da der verfügbare SRAM nicht ausreicht. 

Hinzu kommt, dass für den UART-Betrieb vergleichsweise große Empfangs- und Sendepuffer erforderlich sind (mindestens 300 Byte), während ATmega328P-basierte Boards standardmäßig nur 64 Byte vorsehen.

Auch hinsichtlich ihres Strombedarfs sind die Module recht anspruchsvoll (siehe unten), was ihren Einsatz in batteriebetriebenen Anwendungen einschränken kann.

Wer mit kleineren Mikrocontrollern arbeiten möchte und nicht den vollen Funktionsumfang der BNO08x-Module benötigt, der sollte einen Blick auf den kleinen Bruder, den BNO055, werfen.

BNO08x im Detail

Gängige BNO08x-Module

Ich gehe davon aus, dass die meisten von euch nicht die „nackten“ BNO08x‑ICs, sondern Module verwenden. Hier einige Vertreter, die ich ausprobiert habe:

BNO08x Module: Sparkfun, No-Name und Adafruit

Sie alle funktionieren wunderbar, unterscheiden sich aber in einigen Punkten. So ist die I²C-Adresse beim Sparkfun- und dem No-Name-Modul auf 0x4B voreingestellt (ADO = HIGH). Beim Adafruit-Modul ist die Voreinstellung 0x4A (ADO = LOW). Die beiden Markenprodukte verfügen über den für I²C-Verbindungen sehr praktischen QWIIC-Anschluss. Alle drei Module besitzen Pull-up-Widerstände für die I²C-Leitungen.  

Nur das Adafruit-Modul lässt sich mit 5 Volt betreiben. Es besitzt einen Spannungsregler und Levelshifter für die Ein- und Ausgänge. 

Elektrische Eckdaten

  • Spannungsversorgung: 2.4 – 3.6 Volt (Adafruit-Modul: auch 5V).
  • Stromverbrauch: Der Stromverbrauch hängt von diversen Faktoren ab, wie etwa den Datenraten (siehe Datenblatt, Kapitel 6.10).
    • Als Anhaltspunkt: Ich habe mit dem Sparkfun-Sketch „Example_18_Sleep.ino“ bei 3.3 Volt Spannungsversorgung und I²C-Kommunikation folgende Werte gemessen:
      • Schlafmodus: ca. 0.2 mA.
      • Wachmodus: ca. 12.3 mA.

Kommunikation

Die BNO08x-Vertreter beherrschen I²C, SPI, UART und UART-RVC. UART-RVC ist eine UART-Variante, die nur ein einziges Datenkabel benötigt (RVC = Rotation Vector Channel). Der Nachteil an dieser Variante ist, dass euch nur ein eingeschränkter Funktionsumfang zur Verfügung steht. Außerdem braucht ihr eine separate Bibliothek.

Ihr wählt das Kommunikationsprotokoll über die PS-Pins (Protocoll Selection) aus:

Protokollauswahl über die PS-Pins

Bei allen drei oben abgebildeten Modulen sind PS0 und PS1 mit Pull-Down-Widerständen auf GND gezogen. Über Lötbrücken auf den Rückseiten der Module könnt ihr sie dauerhaft auf HIGH einstellen. Alternativ nutzt ihr dazu die ausgeführten Pins.  

Ausgaben (Reports) der BNO08x-Module

Die Messdaten der BNO08x-Module werden als sogenannte Reports angefordert. Ihr definiert einmalig, was ihr haben wollt, und erhaltet dann kontinuierlich die entsprechenden Daten. Im Prinzip ist das wie bei einem Abonnement. Ihr wisst ungefähr, wann mit den Lieferungen zu rechnen ist, aber den genauen Zeitpunkt wisst ihr erst, wenn die Lieferung tatsächlich eintrifft.

Die Bandbreite der Reports ist beeindruckend. Sie lassen sich in drei Gruppen einteilen:

1. Bewegungsreports (Motion)

Bei diesen Reports handelt es sich um Roh- oder verarbeitete Daten der einzelnen Sensoren. 

  • Beschleunigungswerte:
    • Kalibrierte Beschleunigung des Moduls inklusive der Schwerkraft (in m/s2)
    • Lineare Beschleunigung:  Beschleunigung des Moduls exklusive der Schwerkraft (in m/s2)
    • Erdbeschleunigung: reine Schwerkraft (in m/s2)
    • Unkalibrierte Rohdaten des A/D-Wandlers
  • Winkelbeschleunigung:
    • Kalibrierte Gyroskopwerte (in rad/s)
    • Unkalibrierte Gyroskopdaten (in rad/s)
    • Unkalibrierte Rohdaten des AD/Wandlers
  • Magnetometerwerte:
    • Kalibrierte Magnetometerwerte (µTesla)
    • Unkalibrierte Magnetomerwerte (µTesla)
    • Unkalibrierte Rohdaten des A/D-Wandlers

2. Ausrichtungsreports (Orientation)

Werden die Daten der Sensoren kombiniert („Sensorfusion“), lässt sich die absolute Ausrichtung des Moduls im Raum oder zu einem Bezugspunkt ermitteln. Die Reports geben die Daten in der Regel als Quaternionen aus. Quaternionen sind eine mathematische Methode, um die räumliche Drehung von Körpern zu beschreiben. Um aus Quaternionen anschauliche Daten zu erhalten, sind weitere mathematische Operationen erforderlich. Der Umweg über Quaternionen ist notwendig, da es bei Verwendung von Winkeln („Yaw“, „Pitch“, „Roll“) in bestimmten Grenzfällen zu Problemen wie dem Gimbal Lock kommen kann. 

Die Auswahl des Orientation-Reports ist immer eine Abwägung von Präzision und Geschwindigkeit bzw. Latenz. Zur Auswahl stehen:

  • Geomagnetic Rotation Vector (Geomagnetischer Rotationsvektor): Ausrichtung in Bezug auf den magnetischen Nordpol und die Schwerkraft. Dabei werden nur das Magnetometer und das Accelerometer verwendet, nicht das Gyroskop. 
  • Game Rotation Vector (Rotationsvektor für Spieleanwendungen): Basiert auf dem Accelerometer und dem Gyroskop. Da das Magnetometer nicht verwendet wird, kann der „Yaw“-Winkel (Gierung) über die Zeit driften. Vorteilhaft an dieser Methode ist die geringe Latenz, was sie besonders für Spiele und VR-Anwendungen geeignet macht.
    • AR/VR Stabilized Game Rotation Vector: Korrigierte Variante (siehe Datenblatt, Abschnitt 2.2.3) 
  • Rotation Vector: Nutzt Accelerometer, Gyroskop und Magnetometer und gibt deswegen die genaueste absolute Ausrichtung aus.
    • AR/VR Stabilized Rotation Vector: Korrigierte Variante (siehe Datenblatt, Abschnitt 2.2.5).
  • Gyro-Rotation-Vector: Spezielle Variante des Rotation oder des Game-Rotation-Vectors. (siehe Datenblatt, Abschnitt 2.2.6)
  • Gyro Rotation Vector Prediction: Voraussage, wohin sich der Gyro Protection Vector als Nächstes bewegen wird (siehe Datenblatt, Abschnitt 2.2.7).

3. Klassifizierungsreports (Classification)

Aus der Änderung der Bewegungs- und Orientierungsdaten über die Zeit lassen sich komplexere Bewegungsmuster ableiten. Das sind die sogenannten Klassifizierungsreports: 

  • Stability Detection and Classification:
    • On Table: Sensor liegt absolut ruhig.
    • Stable: Sensor wird ruhig in der Hand gehalten. 
    • Motion: Sensor wird bewegt.
  • Tap Detector: „Antipp“-Detektor.
  • Step Counter: Schrittzähler.
  • Activity Classification: Still (steht), walking (geht), running (rennt).
  • Significant Motion Detector: Signifikante Bewegung.
  • Shake Detector: Schütteldetektor.

Reportfrequenzen

Die Anzahl der Reports pro Sekunde, die die BNO08x-Module liefern können, hängt von der Übertragungsrate des Kommunikationsprotokolls (also I²C, SPI, UART) und von der internen Verarbeitungszeit ab. Außerdem müssen die maximalen Datenraten der zugrundeliegenden Sensoren berücksichtigt werden, wobei nicht alle Sensoren gleichzeitig die maximale Datenrate liefern können. 

Maximale BNO08x Reportfrequenzen
Maximale Datenraten

Wie ihr noch sehen werdet, lässt sich die Zeit zwischen den Reports (im machbaren Rahmen) konfigurieren. Die tatsächliche Reportrate kann jedoch das 0.9- bis 2.1-fache des gewünschten Wertes betragen.

Kalibrierung

Die BNO08x-Module lassen sich dynamisch, also während des Betriebs, kalibrieren:

  • Accelerometer: Entfernen des „Zero-g-Offsets“. In Ruhelage wirkt ausschließlich die Schwerkraft auf den Sensor. Bei der Kalibrierung werden alle Abweichungen davon herausgerechnet.
  • Gyroskop: Entfernen des „Zero-Rate-Offsets“. In Ruhelage sollten die Gyroskopsensoren eine Winkelgeschwindigkeit von 0 anzeigen. Bei der Kalibrierung werden alle Abweichungen davon herausgerechnet.
  • Magnetometer: Entfernen störender Einflüsse auf das Magnetfeld, beispielsweise durch Magnete, Lautsprecher oder ferromagnetische Materialien. 

Je besser die Kalibrierung, desto verlässlicher sind die Messergebnisse. Die Verlässlichkeit der Werte wird auf einer Skala von 0 (unzuverlässig) bis 3 (hohe Zuverlässigkeit) eingestuft. 

Bibliotheken – Sparkfun und Adafruit

In diesem Beitrag betrachte ich die Bibliotheken SparkFun BNO08x Cortex Based IMU und Adafruit BNO08x. Tutorials zu den Bibliotheken und den jeweiligen Modulen findet ihr hier auf den Sparkfun- bzw. hier auf den Adafruit-Seiten.

Grundsätzlich kann man mit beiden Bibliotheken gut arbeiten. Wie auch immer, sie haben ihre Stärken und Schwächen. Für die Sparkfun-Bibliothek spricht (aus meiner Sicht und heutiger Stand!): 

  • Viele Beispielsketche. 
  • Implementierung des Schlafmodus. 
  • Die Kalibrierung ist transparenter (siehe Beispielsketch Nr. 20). 
  • Stabilere I²C-Kommunikation.

Für die Adafruit-Bibliothek spricht:

  • Implementierung von UART (UART-RVC ist in einer separaten Bibliothek implementiert).
  • Einstellung der Reportperiode ist flexibler. Bei der Sparkfun-Bibliothek ist die Periode (derzeit) auf maximal 65 Millisekunden beschränkt. 
  • Die Adafruit-Bibliothek ist näher an der SH-2-Software bzw. der SH-2-API.

Anschluss der BNO08x-Module an den Mikrocontroller (I²C)

Wie ihr euer Modul mit dem Mikrocontroller verbindet, hängt vom Modul und dem gewünschten Kommunikationsprotokoll ab. Ich zeige hier alles am Beispiel des No-Name-Moduls, das über ein ESP32-Development-Board gesteuert wird. Als Protokoll für die Beispielsketche habe ich I²C gewählt. Im Anhang findet ihr die Schaltpläne und Beispielsketche bei Verwendung von SPI, UART und UART-RVC. Außerdem zeige ich, wie ihr 5-Volt-Boards verwendet. 

BNO08x-Modul per I²C am ESP32
BNO08x-Modul per I²C an einem ESP32-Board

In dieser Konfiguration sind die Verbindungen zu PS0, PS1 und ADO redundant, da das No-Name-Board entsprechende Pull-up- bzw. Pull-down-Widerstände hat. Wenn ihr die Adafruit-Bibliothek verwendet, dann braucht ihr auch keine Verbindung zu INT und RST.

Anweisungen für das Wiring des Adafruit-Moduls findet ihr hier, für das Wiring des Sparkfun-Moduls schaut hier

Bei Nutzung der Sparkfun-Bibliothek solltet ihr INT und RST grundsätzlich bei allen Kommunikationsprotokollen verbinden. 

I²C kann mit den BNO08x-Modulen Probleme verursachen (siehe hier). Sollte das bei euch der Fall sein, dann steigt am besten auf SPI um. Auch wenn es um Geschwindigkeit geht, hat SPI die Nase vorn. 

Ausgewählte Beispielsketche

Nun können wir uns endlich der Praxis zuwenden. Ich kann in diesem Beitrag nicht alle Reportfunktionen durchgehen. Stellvertretend zeige ich ein Beispiel für einen Motion-Report, eines für einen Orientation-Report und eines für einen Classification-Report. Abschließend schauen wir uns noch an, wie man mehrere Reports ausgibt. 

Beispiel Motion: Accelerometerwerte

Accelerometerwerte mit Sparkfun

Die Sparkfun-Bibliothek enthält für fast alle Reports einen eigenen Beispielsketch. Ich habe den Beispielsketch Example_02_Accelerometer.ino lediglich hinsichtlich des Reset- und des Interruptpins abgeändert. 

/*
  Using the BNO08x IMU

  This example shows how to output accelerometer values.

  By: Nathan Seidle
  SparkFun Electronics
  Date: December 21st, 2017
  SparkFun code, firmware, and software is released under the MIT License.
  Please see LICENSE.md for further details.

  Originally written by Nathan Seidle @ SparkFun Electronics, December 28th, 2017

  Adjusted by Pete Lewis @ SparkFun Electronics, June 2023 to incorporate the
  CEVA Sensor Hub Driver, found here:
  https://github.com/ceva-dsp/sh2

  Also, utilizing code from the Adafruit BNO08x Arduino Library by Bryan Siepert
  for Adafruit Industries. Found here:
  https://github.com/adafruit/Adafruit_BNO08x

  Also, utilizing I2C and SPI read/write functions and code from the Adafruit
  BusIO library found here:
  https://github.com/adafruit/Adafruit_BusIO

  Hardware Connections: // changed for this blog post by Wolfgang Ewald
  ESP32 --> BNO08x
  19  --> INT 
  18  --> RST

  BNO08x "mode" jumpers set for I2C (default):
  PSO: Open
  PS1: Open

  Serial.print it out at 115200 baud to serial monitor.

  Feel like supporting our work? Buy a board from SparkFun!
  https://www.sparkfun.com/products/22857
*/

#include <Wire.h>

#include "SparkFun_BNO08x_Arduino_Library.h" // CTRL+Click here to get the library: http://librarymanager/All#SparkFun_BNO08x
BNO08x myIMU;

// For the most reliable interaction with the SHTP bus, we need
// to use hardware reset control, and to monitor the H_INT pin.
// The H_INT pin will go low when its okay to talk on the SHTP bus.
// Note, these can be other GPIO if you like.
// Define as -1 to disable these features.
#define BNO08X_INT  19
//#define BNO08X_INT  -1
#define BNO08X_RST  18
//#define BNO08X_RST  -1

#define BNO08X_ADDR 0x4B  // SparkFun BNO08x Breakout (Qwiic) defaults to 0x4B
//#define BNO08X_ADDR 0x4A // Alternate address if ADR jumper is closed

void setup() {
  Serial.begin(115200);
  
  while(!Serial) delay(10); // Wait for Serial to become available.
  // Necessary for boards with native USB (like the SAMD51 Thing+).
  // For a final version of a project that does not need serial debug (or a USB cable plugged in),
  // Comment out this while loop, or it will prevent the remaining code from running.
  
  Serial.println();
  Serial.println("BNO08x Read Example");

  Wire.begin();

  //if (myIMU.begin() == false) {  // Setup without INT/RST control (Not Recommended)
  if (myIMU.begin(BNO08X_ADDR, Wire, BNO08X_INT, BNO08X_RST) == false) {
    Serial.println("BNO08x not detected at default I2C address. Check your jumpers and the hookup guide. Freezing...");
    while (1)
      ;
  }
  Serial.println("BNO08x found!");

  // Wire.setClock(400000); //Increase I2C data rate to 400kHz

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (myIMU.enableAccelerometer() == true) {
    Serial.println(F("Accelerometer enabled"));
    Serial.println(F("Output in form x, y, z, in m/s^2"));
  } else {
    Serial.println("Could not enable accelerometer");
  }
}

void loop() {
  delay(10);

  if (myIMU.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  // Has a new event come in on the Sensor Hub Bus?
  if (myIMU.getSensorEvent() == true) {

    // is it the correct sensor data we want?
    if (myIMU.getSensorEventID() == SENSOR_REPORTID_ACCELEROMETER) {

      float x = myIMU.getAccelX();
      float y = myIMU.getAccelY();
      float z = myIMU.getAccelZ();

      Serial.print(x, 2);
      Serial.print(F(","));
      Serial.print(y, 2);
      Serial.print(F(","));
      Serial.print(z, 2);

      Serial.println();
    }
  }
}

Ich gehe nicht auf jede Zeile ein, denn das Meiste sollte selbsterklärend sein. Mit begin(BNO08X_ADDR, Wire, BNO08X_INT, BNO08X_RST)⁣⁣ initialisiert ihr euer BNO08x-Objekt und übergebt die I²C-Adresse, das Wire-Objekt, den Interrupt-Pin und den Reset-Pin.

In setReports() wird mittels enableAccelerometer() das Accelerometer bzw. die Accelerometer-Reports aktiviert. In loop() fragt getSensorEvent() ab, ob ein Event vorliegt. Wenn ja, dann ermittelt getSensorEventID() die ID des Events. Falls es sich dabei um die Accelerometer-Report-ID handelt, werden die x-, y- und z-Werte abgefragt und ausgegeben. Die Einheit der Werte ist m/s2. Um sie in g-Werte umzurechnen, dividiert ihr sie durch 9.81.

Sollte ein Reset des BNO08x nötig gewesen sein (wasReset() == true), werden die Reports automatisch neu gesetzt.

Hier noch eine beispielhafte Ausgabe:

Ausgabe SF_Example_02_Accelerometer.ino
Ausgabe SF_Example_02_Accelerometer.ino

Accelerometerwerte mit Adafruit

Die Adafruit-Bibliothek ist zwar mit weniger Beispielsketchen ausgestattet, jedoch könnt ihr den Beispielsketch more_reports.ino als Grundlage nehmen und die Teile herausstreichen, die ihr nicht benötigt. So bin auch ich hier vorgegangen.  

// Basic demo for readings from Adafruit BNO08x
#include <Adafruit_BNO08x.h>

// For SPI mode, we need a CS pin
#define BNO08X_CS 10
#define BNO08X_INT 19

// For SPI mode, we also need a RESET 
//#define BNO08X_RESET 5
// but not for I2C or UART
#define BNO08X_RESET -1

Adafruit_BNO08x  bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

void setup(void) {
  Serial.begin(115200);
  while (!Serial) delay(10);     // will pause Zero, Leonardo, etc until serial console opens

  Serial.println("Adafruit BNO08x Accelerometer test!");

  // Try to initialize!
  if (!bno08x.begin_I2C(/* 0x4B */)) {
  //if (!bno08x.begin_UART(&Serial1)) {  // Requires a device with > 300 byte UART buffer!
  //if (!bno08x.begin_SPI(BNO08X_CS, BNO08X_INT)) {
    Serial.println("Failed to find BNO08x chip");
    while (1) { delay(10); }
  }
  Serial.println("BNO08x Found!");

  for (int n = 0; n < bno08x.prodIds.numEntries; n++) {
    Serial.print("Part ");
    Serial.print(bno08x.prodIds.entry[n].swPartNumber);
    Serial.print(": Version :");
    Serial.print(bno08x.prodIds.entry[n].swVersionMajor);
    Serial.print(".");
    Serial.print(bno08x.prodIds.entry[n].swVersionMinor);
    Serial.print(".");
    Serial.print(bno08x.prodIds.entry[n].swVersionPatch);
    Serial.print(" Build ");
    Serial.println(bno08x.prodIds.entry[n].swBuildNumber);
  }

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (! bno08x.enableReport(SH2_ACCELEROMETER)) {
    Serial.println("Could not enable accelerometerame vector");
  }
}


void loop() {
  delay(10);

  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }
  
  if (! bno08x.getSensorEvent(&sensorValue)) {
    return;
  }
  
  switch (sensorValue.sensorId) {
    
    case SH2_ACCELEROMETER:
      Serial.print("Accelerometer - x: ");
      Serial.print(sensorValue.un.accelerometer.x);
      Serial.print(" y: ");
      Serial.print(sensorValue.un.accelerometer.y);
      Serial.print(" z: ");
      Serial.println(sensorValue.un.accelerometer.z);
      break;
  }
}

Zunächst erzeugt ihr wie gehabt ein BNO08x-Objekt. Zusätzlich erzeugt ihr die Struktur sensorValue vom Typ sh2_SensorValue_t, die ihr später der Funktion getSensorEvent() in loop() als Referenz übergebt. sensorValue ist sozusagen ein Formular, in das die Werte eingetragen werden. Es lohnt sich, einen Blick in die Datei sh2_SensorValue.h zu werfen, um den Aufbau dieser Struktur zu verstehen. sh2_SensorValue.h wird auch von der Sparkfun-Bibliothek verwendet – dort aber etwas verborgener hinter den Kulissen.

Mit begin_I2C()⁣initialisiert ihr das Modul. Optional übergebt ihr dabei die I²C-Adresse. Die große for-Schleife in setup() verrät euch Details über das Modul. Das könnt ihr auch weglassen.  

In setupReports() aktiviert ihr mit enableReport(SH2_REPORTNAME) den Report eurer Wahl. Die Reportnamen bzw. die IDs, für die sie stehen, findet ihr entweder im Beispielsketch moreReports.ino oder ihr schaut in die Datei sh2.h, in der die Namen im enum sh2_SensorId_e definiert sind. 

Mir persönlich gefällt die enge Anlehnung der Adafruit-Bibliothek an die SH2-Basis. Ausdrücke wie sensorValue.un.accelerometer.z mögen auf den ersten Blick etwas sperriger erscheinen als ein schlankes myIMU.getAccelZ();, aber ich mag den logischen Aufbau dahinter. 

Ausgabe ada_example_accelerometer.ino

Beispiel Orientation: Rotationsvektor (Euler-Winkel)

Rotationsvektor mit Sparkfun

Der folgende Sketch gibt die Ausrichtung des BNO08x im dreidimensionalen Raum als Yaw-, Pitch- und Roll-Winkel (zu Deutsch: Gier-, Nick- und Rollwinkel) aus. Yaw, Pitch und Roll sind sogenannte Euler-Winkel.

Eigentlich liefert der Rotation Vector Report die Ausrichtung als Quaternion zurück, die Sparkfun-Bibliothek rechnet die Quaternionen im Hintergrund in Euler-Winkel um, die dann noch von rad in Grad gewandelt werden. Abgesehen davon beinhaltet der Sketch eigentlich nichts Neues. 

/*
  Using the BNO08x IMU

  Example : Euler Angles
  By: Paul Clark
  Date: April 28th, 2020

  This example shows how to output the Euler angles: roll, pitch and yaw.
  The yaw (compass heading) is tilt-compensated, which is nice.
  https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
  https://github.com/sparkfun/SparkFun_MPU-9250-DMP_Arduino_Library/issues/5#issuecomment-306509440

  By: Nathan Seidle
  SparkFun Electronics
  Date: December 21st, 2017
  SparkFun code, firmware, and software is released under the MIT License.
  Please see LICENSE.md for further details.

  Originally written by Nathan Seidle @ SparkFun Electronics, December 28th, 2017

  Adjusted by Pete Lewis @ SparkFun Electronics, June 2023 to incorporate the
  CEVA Sensor Hub Driver, found here:
  https://github.com/ceva-dsp/sh2

  Also, utilizing code from the Adafruit BNO08x Arduino Library by Bryan Siepert
  for Adafruit Industries. Found here:
  https://github.com/adafruit/Adafruit_BNO08x

  Also, utilizing I2C and SPI read/write functions and code from the Adafruit
  BusIO library found here:
  https://github.com/adafruit/Adafruit_BusIO

  Hardware Connections:
  IoT RedBoard --> BNO08x
  QWIIC --> QWIIC
  A4  --> INT
  A5  --> RST

  BNO08x "mode" jumpers set for I2C (default):
  PSO: OPEN
  PS1: OPEN

  Serial.print it out at 115200 baud to serial monitor.

  Feel like supporting our work? Buy a board from SparkFun!
  https://www.sparkfun.com/products/22857
*/

#include <Wire.h>

#include "SparkFun_BNO08x_Arduino_Library.h"  // CTRL+Click here to get the library: http://librarymanager/All#SparkFun_BNO08x
BNO08x myIMU;

// For the most reliable interaction with the SHTP bus, we need
// to use hardware reset control, and to monitor the H_INT pin.
// The H_INT pin will go low when its okay to talk on the SHTP bus.
// Note, these can be other GPIO if you like.
// Define as -1 to disable these features.
#define BNO08X_INT  19
//#define BNO08X_INT  -1
#define BNO08X_RST  18
//#define BNO08X_RST  -1

#define BNO08X_ADDR 0x4B  // SparkFun BNO08x Breakout (Qwiic) defaults to 0x4B
//#define BNO08X_ADDR 0x4A // Alternate address if ADR jumper is closed

void setup() {
  Serial.begin(115200);
  
  while(!Serial) delay(10); // Wait for Serial to become available.
  // Necessary for boards with native USB (like the SAMD51 Thing+).
  // For a final version of a project that does not need serial debug (or a USB cable plugged in),
  // Comment out this while loop, or it will prevent the remaining code from running.
  
  Serial.println();
  Serial.println("BNO08x Read Example");

  Wire.begin();

  //if (myIMU.begin() == false) {  // Setup without INT/RST control (Not Recommended)
  if (myIMU.begin(BNO08X_ADDR, Wire, BNO08X_INT, BNO08X_RST) == false) {
    Serial.println("BNO08x not detected at default I2C address. Check your jumpers and the hookup guide. Freezing...");
    while (1)
      ;
  }
  Serial.println("BNO08x found!");

  // Wire.setClock(400000); //Increase I2C data rate to 400kHz

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (myIMU.enableRotationVector() == true) {
    Serial.println(F("Rotation vector enabled"));
    Serial.println(F("Output in form roll, pitch, yaw"));
  } else {
    Serial.println("Could not enable rotation vector");
  }
}

void loop() {
  delay(10);

  if (myIMU.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  // Has a new event come in on the Sensor Hub Bus?
  if (myIMU.getSensorEvent() == true) {

    // is it the correct sensor data we want?
    if (myIMU.getSensorEventID() == SENSOR_REPORTID_ROTATION_VECTOR) {

    float roll = (myIMU.getRoll()) * 180.0 / PI; // Convert roll to degrees
    float pitch = (myIMU.getPitch()) * 180.0 / PI; // Convert pitch to degrees
    float yaw = (myIMU.getYaw()) * 180.0 / PI; // Convert yaw / heading to degrees

    Serial.print(roll, 1);
    Serial.print(F(","));
    Serial.print(pitch, 1);
    Serial.print(F(","));
    Serial.print(yaw, 1);

    Serial.println();
    }
  }
}

Die Funktion enableRotationVector() aktiviert den Rotation Vector Report, der die ID SENSOR_REPORTID_ROTATION_VECTOR hat. Wenn ein entsprechender Report vorliegt, werden die Winkel mit getRoll(), getPitch() und getYaw() abgefragt und von rad in Grad umgerechnet. 

Ausgabe SF_Example_13_EulerAngles.ino
Ausgabe SF_Example_13_EulerAngles.ino
Alternative Rotationsvektoren

Anstelle des Rotation Vectors hättet ihr auch einen anderen Rotationsvektor als Grundlage für die Euler-Winkel auswählen können. Zum Beispiel: Wenn ihr etwa den „AR/VR Stabilized Game Rotation Vector“ verwenden wolltet, dann müsstet ihr Folgendes ersetzen:

  • Zeile 99: enableRotationVector() durch enableARVRStabilizedGameRotationVector(timeBetweenReports). Dabei ist zwingendermaßen die Reportperiode (timeBetweenReports) in Millisekunden zu übergeben. 
  • Zeile 119: SENSOR_REPORTID_ROTATION_VECTOR durch SENSOR_REPORTID_AR_VR_STABILIZED_GAME_ROTATION_VECTOR

Hier könnte die Sparkfun-Bibliothek etwas besser dokumentiert sein. Ich habe deshalb die Rotation-Vector-Report-IDs und die zugehörigen enable-Funktionen in einer Tabelle (s. u.) zusammengefasst. Warum die Reportperiode bei einigen enable-Funktionen nicht vorgegeben ist, ist mir nicht klar. 

Rotation Vector Report-IDs und enable-Funktionen
Rotation-Vector-Report-IDs und enable-Funktionen

Rotationsvektor mit Adafruit

Auch die Adafruit-Bibliothek ist mit einem Beispielsketch für die Ausgabe von Euler-Winkeln ausgestattet. Im Gegensatz zur Sparkfun-Bibliothek findet die Umrechnung der Quaternionen nicht im Hintergrund statt, sondern ist Teil des Sketches.  

Der Sketch blieb bei mir nach einer gewissen Zeit hängen, wenn ich I²C und ein ESP32-Development Board verwendet habe. Adafruit weist darauf hin, dass es mit I²C in Verbindung mit einigen Boards Probleme geben kann (siehe hier). Mit SPI gab es bei mir keine Probleme.  

#include <Arduino.h>
// This demo explores two reports (SH2_ARVR_STABILIZED_RV and SH2_GYRO_INTEGRATED_RV) both can be used to give 
// quartenion and euler (yaw, pitch roll) angles.  Toggle the FAST_MODE define to see other report.  
// Note sensorValue.status gives calibration accuracy (which improves over time)
#include <Adafruit_BNO08x.h>

// For SPI mode, we need a CS pin
#define BNO08X_CS 10
#define BNO08X_INT 19


// #define FAST_MODE

// For SPI mode, we also need a RESET 
//#define BNO08X_RESET 5
// but not for I2C or UART
#define BNO08X_RESET -1

struct euler_t {
  float yaw;
  float pitch;
  float roll;
} ypr;

Adafruit_BNO08x  bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

#ifdef FAST_MODE
  // Top frequency is reported to be 1000Hz (but freq is somewhat variable)
  sh2_SensorId_t reportType = SH2_GYRO_INTEGRATED_RV;
  long reportIntervalUs = 2000;
#else
  // Top frequency is about 250Hz but this report is more accurate
  sh2_SensorId_t reportType = SH2_ARVR_STABILIZED_RV;
  long reportIntervalUs = 5000;
#endif
void setReports(sh2_SensorId_t reportType, long report_interval) {
  Serial.println("Setting desired reports");
  if (! bno08x.enableReport(reportType, report_interval)) {
    Serial.println("Could not enable stabilized remote vector");
  }
}

void setup(void) {

  Serial.begin(115200);
  while (!Serial) delay(10);     // will pause Zero, Leonardo, etc until serial console opens

  Serial.println("Adafruit BNO08x test!");

  // Try to initialize!
  if (!bno08x.begin_I2C(/* 0x4B */)) {  // ADO = HIGH => I2C Address = 0x4B
  //if (!bno08x.begin_UART(&Serial1)) {  // Requires a device with > 300 byte UART buffer!
  //if (!bno08x.begin_SPI(BNO08X_CS, BNO08X_INT)) {
    Serial.println("Failed to find BNO08x chip");
    while (1) { delay(10); }
  }
  Serial.println("BNO08x Found!");


  setReports(reportType, reportIntervalUs);

  Serial.println("Reading events");
  delay(100);
}

void quaternionToEuler(float qr, float qi, float qj, float qk, euler_t* ypr, bool degrees = false) {

    float sqr = sq(qr);
    float sqi = sq(qi);
    float sqj = sq(qj);
    float sqk = sq(qk);

    ypr->yaw = atan2(2.0 * (qi * qj + qk * qr), (sqi - sqj - sqk + sqr));
    ypr->pitch = asin(-2.0 * (qi * qk - qj * qr) / (sqi + sqj + sqk + sqr));
    ypr->roll = atan2(2.0 * (qj * qk + qi * qr), (-sqi - sqj + sqk + sqr));

    if (degrees) {
      ypr->yaw *= RAD_TO_DEG;
      ypr->pitch *= RAD_TO_DEG;
      ypr->roll *= RAD_TO_DEG;
    }
}

void quaternionToEulerRV(sh2_RotationVectorWAcc_t* rotational_vector, euler_t* ypr, bool degrees = false) {
    quaternionToEuler(rotational_vector->real, rotational_vector->i, rotational_vector->j, rotational_vector->k, ypr, degrees);
}

void quaternionToEulerGI(sh2_GyroIntegratedRV_t* rotational_vector, euler_t* ypr, bool degrees = false) {
    quaternionToEuler(rotational_vector->real, rotational_vector->i, rotational_vector->j, rotational_vector->k, ypr, degrees);
}

void loop() {

  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports(reportType, reportIntervalUs);
  }
  
  if (bno08x.getSensorEvent(&sensorValue)) {
    // in this demo only one report type will be received depending on FAST_MODE define (above)
    switch (sensorValue.sensorId) {
      case SH2_ARVR_STABILIZED_RV:
        quaternionToEulerRV(&sensorValue.un.arvrStabilizedRV, &ypr, true);
      case SH2_GYRO_INTEGRATED_RV:
        // faster (more noise?)
        quaternionToEulerGI(&sensorValue.un.gyroIntegratedRV, &ypr, true);
        break;
    }
    static long last = 0;
    long now = micros();
    Serial.print(now - last);             Serial.print("\t");
    last = now;
    Serial.print(sensorValue.status);     Serial.print("\t");  // This is accuracy in the range of 0 to 3
    Serial.print(ypr.yaw);                Serial.print("\t");
    Serial.print(ypr.pitch);              Serial.print("\t");
    Serial.println(ypr.roll);
  }

}

 

Wie ihr seht, könnt ihr in diesem Beispiel über den Schalter #define FAST_MODE zwischen dem AR/VR Stabilized Rotation Vector und dem Gyro Rotation Vector (ist schneller, aber ungenauer) als Grundlage wechseln. 

Zusätzlich zu den Euler-Winkeln gibt der Sketch die Zeit zwischen den Reportausgaben und die Verlässlichkeit der Werte (Accuracy) aus. Das folgende Ausgabebeispiel zeigt, dass die Zeit recht erheblich von der Vorgabe (5000 µs) abweichen kann:

Ausgabe Ada_quaternion_yaw_pitch_roll.ino
Ausgabe Ada_quaternion_yaw_pitch_roll.ino

Es ist auch möglich, andere Rotation Vector Reports zu verwenden. Allerdings müsst ihr dazu die Namen der Report-IDs und die zugehörigen Variablentypen kennen. Ihr findet sie in sh2_SensorValue.h oder in dieser Tabelle:

Rotation Vector Report_IDs und Variablentypen
Rotation Vector Report_IDs und Variablentypen

Beispiel Classification – Schrittzähler

Als Beispiel für einen Classification-Report schauen wir uns den Schrittzähler an. 

Schrittzähler mit Sparkfun

Abgesehen davon, dass wir hier den Step-Counter-Report aktivieren, findet sich nichts grundlegend Neues in diesem Sketch. 

/*
  Using the BNO08x IMU

  This example shows the step count. Tap the IC a few times to emulate a step.

  By: Nathan Seidle
  SparkFun Electronics
  Date: December 21st, 2017
  SparkFun code, firmware, and software is released under the MIT License.
  Please see LICENSE.md for further details.

  Originally written by Nathan Seidle @ SparkFun Electronics, December 28th, 2017

  Adjusted by Pete Lewis @ SparkFun Electronics, June 2023 to incorporate the
  CEVA Sensor Hub Driver, found here:
  https://github.com/ceva-dsp/sh2

  Also, utilizing code from the Adafruit BNO08x Arduino Library by Bryan Siepert
  for Adafruit Industries. Found here:
  https://github.com/adafruit/Adafruit_BNO08x

  Also, utilizing I2C and SPI read/write functions and code from the Adafruit
  BusIO library found here:
  https://github.com/adafruit/Adafruit_BusIO

  Hardware Connections:
  IoT RedBoard --> BNO08x
  QWIIC --> QWIIC
  A4  --> INT
  A5  --> RST

  BNO08x "mode" jumpers set for I2C (default):
  PSO: OPEN
  PS1: OPEN

  Serial.print it out at 115200 baud to serial monitor.

  Feel like supporting our work? Buy a board from SparkFun!
  https://www.sparkfun.com/products/22857
*/

#include <Wire.h>

#include "SparkFun_BNO08x_Arduino_Library.h" // CTRL+Click here to get the library: http://librarymanager/All#SparkFun_BNO08x
BNO08x myIMU;

// For the most reliable interaction with the SHTP bus, we need
// to use hardware reset control, and to monitor the H_INT pin.
// The H_INT pin will go low when its okay to talk on the SHTP bus.
// Note, these can be other GPIO if you like.
// Define as -1 to disable these features.
#define BNO08X_INT  19
//#define BNO08X_INT  -1
#define BNO08X_RST  18
//#define BNO08X_RST  -1

#define BNO08X_ADDR 0x4B  // SparkFun BNO08x Breakout (Qwiic) defaults to 0x4B
//#define BNO08X_ADDR 0x4A // Alternate address if ADR jumper is closed

void setup() {
  Serial.begin(115200);
  
  while(!Serial) delay(10); // Wait for Serial to become available.
  // Necessary for boards with native USB (like the SAMD51 Thing+).
  // For a final version of a project that does not need serial debug (or a USB cable plugged in),
  // Comment out this while loop, or it will prevent the remaining code from running.
  
  Serial.println();
  Serial.println("BNO08x Read Example");

  Wire.begin();

  //if (myIMU.begin() == false) {  // Setup without INT/RST control (Not Recommended)
  if (myIMU.begin(BNO08X_ADDR, Wire, BNO08X_INT, BNO08X_RST) == false) {
    Serial.println("BNO08x not detected at default I2C address. Check your jumpers and the hookup guide. Freezing...");
    while (1)
      ;
  }
  Serial.println("BNO08x found!");

  // Wire.setClock(400000); //Increase I2C data rate to 400kHz

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (myIMU.enableStepCounter(65) == true) { //Send data update every 65 ms (maximum)
    Serial.println(F("Step Counter enabled"));
    Serial.println(F("Step count since sketch started:"));
  } else {
    Serial.println("Could not step counter");
  }
}

void loop() {
  delay(500); // step counter needs a little longer of delay (200ms or more)

  if (myIMU.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  // Has a new event come in on the Sensor Hub Bus?
  if (myIMU.getSensorEvent() == true) {

    // is it the correct sensor data we want?
    if (myIMU.getSensorEventID() == SENSOR_REPORTID_STEP_COUNTER) {

      unsigned int steps = myIMU.getStepCount();

      Serial.print(steps);

      Serial.println();
    }
  }
}

Hier ein Ausschnitt der Ausgabe des Sketches. Um die Funktion zu testen, habe ich mit dem Breadboard in der Hand ein paar Schritte im Stand vor meinem Schreibtisch gemacht (was wahrscheinlich ziemlich bescheuert aussah). Es funktioniert richtig gut. 

Ausgabe von SF_Example_05_StepCounter.ino
Ausgabe von SF_Example_05_StepCounter.ino

Schrittzähler mit Adafruit

Auch der Adafruit-Sketch für den Schrittzähler ist eigentlich wenig überraschend. Nur ein Aspekt ist neu, und zwar wird hier die Latenz in Mikrosekunden mit sensorValue.un.stepCounter.latency ausgegeben. Die Latenz ist die Verzögerung zwischen der Bewegung und dem Auslösen des Events.

// Basic demo for readings from Adafruit BNO08x
#include <Adafruit_BNO08x.h>

// For SPI mode, we need a CS pin
#define BNO08X_CS 10
#define BNO08X_INT 19

// For SPI mode, we also need a RESET
//#define BNO08X_RESET 5
// but not for I2C or UART
#define BNO08X_RESET -1

Adafruit_BNO08x bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

void setup(void) {
  Serial.begin(115200);
  while (!Serial)
    delay(10); // will pause Zero, Leonardo, etc until serial console opens

  Serial.println("Adafruit BNO08x test!");

  // Try to initialize!
  if (!bno08x.begin_I2C()) {
    // if (!bno08x.begin_UART(&Serial1)) {  // Requires a device with > 300 byte
    // UART buffer! if (!bno08x.begin_SPI(BNO08X_CS, BNO08X_INT)) {
    Serial.println("Failed to find BNO08x chip");
    while (1) {
      delay(10);
    }
  }
  Serial.println("BNO08x Found!");

  setReports();

  Serial.println("Reading Step Counter events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (!bno08x.enableReport(SH2_STEP_COUNTER)) {
    Serial.println("Could not enable step counter");
 }
}

void loop() {
  delay(10);

  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  if (!bno08x.getSensorEvent(&sensorValue)) {
    return;
  }

  switch (sensorValue.sensorId) {

  case SH2_STEP_COUNTER:
    Serial.print("Step Counter - steps: ");
    Serial.print(sensorValue.un.stepCounter.steps);
    Serial.print(" latency: ");
    Serial.println(sensorValue.un.stepCounter.latency);
    break;
  }
}

Hier noch ein Ausschnitt der Ausgabe:

Ausgabe von ada_step_counter.ino
Ausgabe von ada_step_counter.ino

Die Latenzwerte sind während des Gehens ausgesprochen gering. Wenn ihr nach einer kurzen Pause wieder startet, dann könnt ihr sehen, dass die Latenz für den ersten Schritt erheblich größer ist. 

Reports kombinieren

Bisher haben wir nur jeweils einen Report aktiviert. Ihr könnt aber auch mehrere Reports parallel aktivieren. 

„Ungeordnete“ Ausgabe am Beispiel Adafruit

Hier beginne ich mit der Adafruit-Bibliothek, da es dazu einen Beispielsketch gibt (more_reports.ino). An dem Screenshot unten könnt ihr sehen, dass die Reports ziemlich ungeordnet ausgegeben werden. Bei Anwendungen, bei denen auf bestimmte Zustände reagiert werden soll, ist das auch OK so. 

Ausgabe more_reports.ino
Ausgabe more_reports.ino

Schrittzähler und Stability Classification mit Sparkfun

Anhand des Schrittzählers und der Activity Classification schauen wir uns an, wie ihr zwei Reports kombiniert und zeitlich steuert. 

Die Zeit zwischen den Reports lässt sich bei der Sparkfun-Bibliothek mit enablexxxx(timeBetweenReports) einstellen. Dabei ist xxxx der jeweilige Report und timeBetweenReports die Periode in Millisekunden. Leider lässt sich die Periode (derzeit) nur auf maximal 65 Millisekunden ausdehnen. Voreingestellt sind 10 Millisekunden. 

Mein Ziel war es, ca. jede Sekunde eine Ausgabe der beiden Reports zu erzeugen. Das ist jenseits der 65 Millisekunden, und deshalb steuert der folgende Sketch die Ausgabefrequenz des Schrittzählers über millis(). Die Ausgabe der Stability Classification erfolgt nur, wenn zuvor die Schritte ausgegeben wurden. 

#include <Wire.h>
#include "SparkFun_BNO08x_Arduino_Library.h" // CTRL+Click here to get the library: http://librarymanager/All#SparkFun_BNO08x

BNO08x myIMU;

#define BNO08X_INT  19
//#define BNO08X_INT  -1
#define BNO08X_RST  18
//#define BNO08X_RST  -1

#define BNO08X_ADDR 0x4B  // SparkFun BNO08x Breakout (Qwiic) defaults to 0x4B
//#define BNO08X_ADDR 0x4A // Alternate address if ADR jumper is closed

#define OUTPUT_PERIOD_MS 1000 // One output per second

void setup() {
  Serial.begin(115200);
  
  while(!Serial) delay(10); 

  Serial.println();
  Serial.println("BNO08x Read Example");

  Wire.begin();

  //if (myIMU.begin() == false) {  // Setup without INT/RST control (Not Recommended)
  if (myIMU.begin(BNO08X_ADDR, Wire, BNO08X_INT, BNO08X_RST) == false) {
    Serial.println("BNO08x not detected at default I2C address. Check your jumpers and the hookup guide. Freezing...");
    while (1)
      ;
  }
  Serial.println("BNO08x found!");

  // Wire.setClock(400000); //Increase I2C data rate to 400kHz

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  
  if (myIMU.enableStepCounter(65) == true) { //Send data update every 65 (
    Serial.println(F("Step Counter enabled"));
    Serial.println(F("Step count since sketch started:"));
  } else {
    Serial.println("Could not step counter");
  }

  if (myIMU.enableStabilityClassifier(65) == true) {
    Serial.println(F("Stability Classifier enabled"));
  } else {
    Serial.println("Could not enable Stability Classifier");
  }
}

void loop() {
  static unsigned long lastUpdateStepCounter = 0;
  static bool lastUpdateWasStepCounter = false;
   
  if (myIMU.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  if (myIMU.getSensorEvent() == true) {
  
    // is it the correct sensor data we want?
    if (myIMU.getSensorEventID() == SENSOR_REPORTID_STEP_COUNTER) {
      if(millis() - lastUpdateStepCounter > OUTPUT_PERIOD_MS){
        unsigned int steps = myIMU.getStepCount();

        Serial.print("Steps: ");
        Serial.println(steps);
        lastUpdateWasStepCounter = true;
        lastUpdateStepCounter = millis();
      }
    }

    if (myIMU.getSensorEventID() == SENSOR_REPORTID_STABILITY_CLASSIFIER) {
      if(lastUpdateWasStepCounter){
        byte classification = myIMU.getStabilityClassifier();

        if(classification == STABILITY_CLASSIFIER_UNKNOWN) Serial.print(F("Unknown classification"));
        else if(classification == STABILITY_CLASSIFIER_ON_TABLE) Serial.print(F("On table"));
        else if(classification == STABILITY_CLASSIFIER_STATIONARY) Serial.print(F("Stationary"));
        else if(classification == STABILITY_CLASSIFIER_STABLE) Serial.print(F("Stable"));
        else if(classification == STABILITY_CLASSIFIER_MOTION) Serial.print(F("Motion"));
        else if(classification == STABILITY_CLASSIFIER_RESERVED) Serial.print(F("[Reserved]"));

        Serial.println();
        lastUpdateWasStepCounter = false;
      }
    }
  }
}

Hier nun die Ausgabe:

Ausgabe von sf_steps_and_stability.ino

Schrittzähler und Stability Classification mit Adafruit

Mit der Adafruit-Bibliothek ist es einfacher, die Ausgaben zeitlich aufeinander abzustimmen, da es keine Beschränkung hinsichtlich der Zeit zwischen den Reports gibt (außer, dass es sich um einen uint32_t-Wert handelt). Ihr übergebt diese Zeit der Funktion enableReport() als zweiten Parameter in Mikrosekunden. 

// Basic demo for readings from Adafruit BNO08x
#include <Adafruit_BNO08x.h>

// For SPI mode, we need a CS pin
#define BNO08X_CS 10
#define BNO08X_INT 19

// For SPI mode, we also need a RESET
//#define BNO08X_RESET 5
// but not for I2C or UART
#define BNO08X_RESET -1

Adafruit_BNO08x bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

void setup(void) {
  Serial.begin(115200);
  while (!Serial)
    delay(10); // will pause Zero, Leonardo, etc. until serial console opens

  Serial.println("Adafruit BNO08x test!");

  // Try to initialize!
  if (!bno08x.begin_I2C()) {
    // if (!bno08x.begin_UART(&Serial1)) {  // Requires a device with > 300 byte
    // UART buffer! if (!bno08x.begin_SPI(BNO08X_CS, BNO08X_INT)) {
    Serial.println("Failed to find BNO08x chip");
    while (1) {
      delay(10);
    }
  }
  Serial.println("BNO08x Found!");

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (!bno08x.enableReport(SH2_STEP_COUNTER, 2000000)) { // period: 2s
    Serial.println("Could not enable step counter");
  }
  if (!bno08x.enableReport(SH2_STABILITY_CLASSIFIER, 2000000)) {  //period: 2s
    Serial.println("Could not enable stability classifier");
  }
}

void loop() {
  delay(10);
  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  if (!bno08x.getSensorEvent(&sensorValue)) {
    return;
  }

  switch (sensorValue.sensorId) {

    case SH2_STEP_COUNTER:
      Serial.print("Step Counter - steps: ");
      Serial.print(sensorValue.un.stepCounter.steps);
      Serial.print(" latency: ");
      Serial.println(sensorValue.un.stepCounter.latency);
      break;

    case SH2_STABILITY_CLASSIFIER: {
      Serial.print("Stability Classification: ");
      sh2_StabilityClassifier_t stability = sensorValue.un.stabilityClassifier;
      switch (stability.classification) {
      case STABILITY_CLASSIFIER_UNKNOWN:
        Serial.println("Unknown");
        break;
      case STABILITY_CLASSIFIER_ON_TABLE:
        Serial.println("On Table");
        break;
      case STABILITY_CLASSIFIER_STATIONARY:
        Serial.println("Stationary");
        break;
      case STABILITY_CLASSIFIER_STABLE:
        Serial.println("Stable");
        break;
      case STABILITY_CLASSIFIER_MOTION:
        Serial.println("In Motion");
        break;
      }
      break;
    }
  }
}

 

Hier die zugehörige Ausgabe:

Ausgabe von Ada_step_and_stability.ino
Ausgabe von Ada_step_and_stability.ino

Anhang – SPI, UART, UART-RVC, 5-Volt-Boards

Im Anhang findet ihr noch Schaltungen und Sketche für alternative Kommunikationsprotokolle und ein Beispiel für den Anschluss an ein 5-Volt-basiertes Board. 

SPI-Steuerung der BNO08x-Module

Hier ein Beispiel für den Anschluss des No-Name-BNO08x-Moduls an ein ESP32-Development Board per SPI:

Anschluss eines BNO08x an ein ESP32-Board per SPI
BNO08x am ESP32-Board per SPI

Wenn ihr Kabel sparen wollt, dann zieht ihr PS0 und PS1 durch Verbinden der Lötpads auf der Rückseite des Moduls hoch. Wenn ihr nur ein SPI-Gerät an der SPI-Schnittstelle verwendet, dann könntet ihr noch CS dauerhaft auf GND ziehen. 

SPI mit Sparkfun

So sieht der Accelerometer-Sketch für den SPI-Anschluss aus:

#include "SparkFun_BNO08x_Arduino_Library.h" 
BNO08x myIMU;

#define BNO08X_CS   5
#define BNO08X_INT  22
#define BNO08X_RST  4

void setup() {
  Serial.begin(115200);
  
  while(!Serial) delay(10); // Wait for Serial to become available.
  
  Serial.println();
  Serial.println("BNO08x Read Example");

 if (myIMU.beginSPI(BNO08X_CS, BNO08X_INT, BNO08X_RST) == false) {
    Serial.println("BNO08x not detected. Check your jumpers and the hookup guide. Freezing...");
    while (1)
      ;
  }
  Serial.println("BNO08x found!");

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (myIMU.enableAccelerometer() == true) {
    Serial.println(F("Accelerometer enabled"));
    Serial.println(F("Output in form x, y, z, in m/s^2"));
  } else {
    Serial.println("Could not enable accelerometer");
  }
  delay(100); // important for SPI!
  
}

void loop() {
  delay(10);

  if (myIMU.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }

  // Has a new event come in on the Sensor Hub Bus?
  if (myIMU.getSensorEvent() == true) {

    // is it the correct sensor data we want?
    if (myIMU.getSensorEventID() == SENSOR_REPORTID_ACCELEROMETER) {

      float x = myIMU.getAccelX();
      float y = myIMU.getAccelY();
      float z = myIMU.getAccelZ();

      Serial.print(x, 2);
      Serial.print(F(","));
      Serial.print(y, 2);
      Serial.print(F(","));
      Serial.print(z, 2);

      Serial.println();
    }
  }
}

SPI mit Adafruit

Hier die Adafruit SPI-Version:

#include <Adafruit_BNO08x.h>

#define BNO08X_CS 5
#define BNO08X_INT 22
#define BNO08X_RESET 4

Adafruit_BNO08x  bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

void setup(void) {
  Serial.begin(115200);
  while (!Serial) delay(10);     // will pause Zero, Leonardo, etc until serial console opens

  Serial.println("Adafruit BNO08x Accelerometer test!");

  // Try to initialize!
  if (!bno08x.begin_SPI(BNO08X_CS, BNO08X_INT)) {
    Serial.println("Failed to find BNO08x chip");
    while (1) { delay(10); }
  }
  Serial.println("BNO08x Found!");

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (! bno08x.enableReport(SH2_ACCELEROMETER)) {
    Serial.println("Could not enable accelerometerame vector");
  }
}


void loop() {
  delay(10);

  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }
  
  if (! bno08x.getSensorEvent(&sensorValue)) {
    return;
  }
  
  switch (sensorValue.sensorId) {
    
    case SH2_ACCELEROMETER:
      Serial.print("Accelerometer - x: ");
      Serial.print(sensorValue.un.accelerometer.x);
      Serial.print(" y: ");
      Serial.print(sensorValue.un.accelerometer.y);
      Serial.print(" z: ");
      Serial.println(sensorValue.un.accelerometer.z);
      break;
  }
}

UART-Steuerung (nur Adafruit)

Die UART-Steuerung ist nur in der Adafruit-Bibliothek implementiert. Es empfiehlt sich, nicht dieselbe Serial-Schnittstelle wie für die Verbindung zum seriellen Monitor zu nehmen. In meiner Beispielschaltung und dem zugehörigen Sketch nutze ich die Pins 18 und 19 als RX2 bzw. TX2. 

Falls ihr ein Board wie das unten abgebildete verwendet: Ich hätte auch die als RX2 und TX2 gelabelten Pins nehmen können. Allerdings sind die den GPIOs 16 und 17 zugeordnet. Im ESP32-Boardpaket sind RX2 und TX2 aber den Pins 4 und 25 zugeordnet. Wegen dieser Verwirrung habe ich mich für andere Pins entschieden.

BNO08x am ESP32 per UART
BNO08x am ESP32 per UART

Hier nun der Beispielsketch dazu:

#include <Adafruit_BNO08x.h>

#define BNO08X_RESET -1
#define RX2 18
#define TX2 19

Adafruit_BNO08x  bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

void setup(void) {
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, RX2, TX2);
  while (!Serial) delay(10);     // will pause Zero, Leonardo, etc until serial console opens
  while (!Serial2) delay(10); 

  Serial.println("Adafruit BNO08x Accelerometer test!");

  // Try to initialize!
  if (!bno08x.begin_UART(&Serial2)) {  // Requires a device with > 300 byte UART buffer!
    Serial.println("Failed to find BNO08x chip");
    while (1) { delay(10); }
  }
  Serial.println("BNO08x Found!");

  setReports();

  Serial.println("Reading events");
  delay(100);
}

// Here is where you define the sensor outputs you want to receive
void setReports(void) {
  Serial.println("Setting desired reports");
  if (! bno08x.enableReport(SH2_ACCELEROMETER)) {
    Serial.println("Could not enable accelerometerame vector");
  }
}


void loop() {
  delay(10);

  if (bno08x.wasReset()) {
    Serial.print("sensor was reset ");
    setReports();
  }
  
  if (! bno08x.getSensorEvent(&sensorValue)) {
    return;
  }
  
  switch (sensorValue.sensorId) {
    
    case SH2_ACCELEROMETER:
      Serial.print("Accelerometer - x: ");
      Serial.print(sensorValue.un.accelerometer.x);
      Serial.print(" y: ");
      Serial.print(sensorValue.un.accelerometer.y);
      Serial.print(" z: ");
      Serial.println(sensorValue.un.accelerometer.z);
      break;
  }
}

UART-RVC-Steuerung (nur Adafruit)

Für die Variante UART-RVC müsst ihr eine andere Bibliothek wie etwa Adafruit BNO08x RVC nutzen. Sie hat einen sehr eingeschränkten Funktionsumfang, was aber nicht an der Bibliothek, sondern an der SH-2 Firmware liegt. Ihr könnt damit lediglich den Rotationsvektor und die Accelerometerwerte ermitteln. Wie auch immer, für einige Anwendungen ist das ausreichend. 

Hier das sehr übersichtliche Wiring:

BNO08x am ESP32 per UART-RVC
BNO08x am ESP32 per UART-RVC

Und hier der zugehörige Beispielsketch:

#include "Adafruit_BNO08x_RVC.h"

#define RX2 18
#define TX2 19

Adafruit_BNO08x_RVC rvc = Adafruit_BNO08x_RVC();

void setup() {
  // Wait for serial monitor to open
  Serial.begin(115200);
  Serial2.begin(115200, SERIAL_8N1, RX2, TX2);
  while (!Serial) delay(10);
  while (!Serial2) delay(10); 

  Serial.println("Adafruit BNO08x IMU - UART-RVC mode");

  Serial1.begin(115200); // This is the baud rate specified by the datasheet
  while (!Serial1)
    delay(10);

  if (!rvc.begin(&Serial2)) { // connect to the sensor over hardware serial
    Serial.println("Could not find BNO08x!");
    while (1)
      delay(10);
  }

  Serial.println("BNO08x found!");
}

void loop() {
  BNO08x_RVC_Data heading;

  if (!rvc.read(&heading)) {
    return;
  }

  Serial.println();
  Serial.println(F("---------------------------------------"));
  Serial.println(F("Principal Axes:"));
  Serial.println(F("---------------------------------------"));
  Serial.print(F("Yaw: "));
  Serial.print(heading.yaw);
  Serial.print(F("\tPitch: "));
  Serial.print(heading.pitch);
  Serial.print(F("\tRoll: "));
  Serial.println(heading.roll);
  Serial.println(F("---------------------------------------"));
  Serial.println(F("Acceleration"));
  Serial.println(F("---------------------------------------"));
  Serial.print(F("X: "));
  Serial.print(heading.x_accel);
  Serial.print(F("\tY: "));
  Serial.print(heading.y_accel);
  Serial.print(F("\tZ: "));
  Serial.println(heading.z_accel);
  Serial.println(F("---------------------------------------"));


  //  delay(200);
}

5-Volt-Boards nutzen am Beispiel UNO R4 Minima

Und zum Schluss wollte ich noch zeigen, dass ihr natürlich auch 5-Volt-Boards wie den Arduino UNO R4 Minima verwenden könnt. Wenn ihr kein 5-Volt-fähiges BNO08x-Modul wie das von Adafruit habt, müsst ihr einen Levelshifter (z.B. den TXS0108E) verwenden. Trotzdem gilt natürlich immer noch, dass euer Mikrocontrollerboard den hohen Ansprüchen an den Arbeitsspeicher genügen muss.

Hier beispielhaft die Schaltung für einen Arduino UNO R4 Minima mit TXS0108E-Levelshifter bei Verwendung von SPI:

BNO08x-Modul am Arduino UNO R4 Minima
BNO08x-Modul am Arduino UNO R4 Minima

Das ist schon recht viel Steck- oder Lötarbeit. 

Danksagung

Das Koordinatenkreuz im Vordergrund meines Beitragsbilds (Link) stammt von Clker-Free-Vector-Images auf Pixabay.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert