ICM-20948 9-Achsensensor Teil I

Über den Beitrag

In diesem Beitrag stelle ich mit dem ICM-20948 einen weiteren 9-Achsensensor vor. Vor ein paar Monaten hatte ich schon über seinen Vorgänger, den MPU-9250, berichtet. Der MPU-9250 ist vom Hersteller als „EoL“ (End of Life) gekennzeichnet. Aber auch, wenn er sicherlich noch eine ganze Weile erhältlich sein sollte, ist es an der Zeit, sich den Nachfolger näher anzuschauen.

Zumindest nach außen sind die Unterschiede zwischen dem ICM-20948 und dem MPU-9250 nicht wirklich groß. Wenn ihr mit meiner Bibliothek zum MPU-9250 gearbeitet habt, werdet ihr deshalb auch schnell mit meiner Bibliothek zum ICM-20948 zurechtkommen.

„Unter der Haube“, also auf Registerebene, sind die Unterschiede erheblich. Auch, wenn es anders wirkt – das Erstellen der Bibliothek war deshalb mehr als eine kleine Modifikation der MPU-9250 Bibliothek.

Eigenschaften und technische Daten des ICM-20948

Der ICM-20948 heißt 9-Achsensensor, da er drei Sensoren vereinigt, die jeweils drei Achsen abdecken. Er ist:

  • Beschleunigungssensor (Akzelerometer),
  • Rotationsgeschwindigkeitssensor (Gyroskop),
  • und Magnetometer.
  • Darüber hinaus besitzt der ICM-20948 ein Thermometer.

Oft findet man für solche kombinierten Sensoren auch die Bezeichnung IMU für „Inertial Measurement Unit“.

Wie ein Gyroskop und ein Beschleunigungssensor funktionieren, habe ich schon in meinem Beitrag über den MPU6050 beschrieben. Das Magnetometer funktioniert auf Basis des Hall-Effekts. Der ist hier sehr anschaulich erklärt.

 

ICM-20948 Modul

Die wesentlichen technischen Daten des ICM-20948 sind:

  • Spannungsversorgung: 1.71 – 3.6 Volt
    • die meisten Module haben keinen Spannungsregler
  • VDDIO: 1.71 – 1.95 Volt
    • Toleranzbereich für I/O-Pins
  • Kommunikation über I2C, Adressen:
    • AD0 an LOW: 0x68
    • AD0 an HIGH: 0x69
  • Kommunikation über SPI
  • Gyroskop:
    • Messbereiche: +/-250, +/-500, +/-1000, +/-2000 °/s
    • Datenrate: 4.4 Hz – 9 kHz
  • Beschleunigungssensor:
    • Messbereiche: +/- 2, +/-4, +/-8, +/-16 g
    • Datenrate: 4.5 Hz – 4.5 kHz
  • Magnetometer (AK09916):
    • Messbereich: +/- 4900 µT
    • max. Datenrate: 100 Hz
  • FIFO (first in, first out) Datenspeicher: 512 / 4096 Bytes
  • Interrupts (die von mir implementierten): FIFO Überlauf, Data Ready, FSync und „Wake-on-Motion“ (Beschleunigungsinterrupt)
  • Integriertes Thermometer

Der ICM-20948 ermöglicht es, bis zu fünf weitere Sensoren mit einer I2C Hilfsschnittstelle (Auxiliary Interface) zu steuern. Die Messwerte werden in Registern des ICM-20948 gespeichert und von dort ausgelesen.

Das Magnetometer ist nicht wirklich in den ICM-20948 integriert. Es besitzt eigene Register und eine eigene I2C Adresse. Ihr könnt es nur über die eben beschriebene Hilfsschnittstelle erreichen. Aber keine Sorge, ihr müsst euch nicht damit auseinandersetzen, denn das macht meine Bibliothek im Hintergrund.

Weitere Informationen findet ihr im technischen Datenblatt des ICM-20948 und im Datenblatt des Magnetometers AK09916. Über die Unterschiede zwischen dem MPU-9250 und dem ICM-20948 könnt ihr euch hier informieren.

Digital Motion Processor DMP

Der ICM-20948 bietet die Möglichkeit, die von ihm und weiteren externen Sensoren gemessenen Daten zu verarbeiten. Das entlastet den steuernden Mikrocontroller. Zu diesem Zweck besitzt der ICM-20948 den sogenannten Digital Motion ProcessorTM, kurz DMP. Allerdings wäre die Implementierung der DMP Funktionen ein Aufwand für mich, der meine Kapazitäten übersteigt. In die Entwicklung der Bibliothek habe auch ohne das schon ein paar dutzend Stunden gesteckt. Außerdem würden die meisten Hobbyanwender die DMP Funktionen wohl gar nicht nutzen.

Ansteuerung

Schaltung

Bei der Spannungsversorgung müsst ihr darauf achten, dass die meisten ICM-20948 Module nur mit bis zu 3.6 Volt betrieben werden können. Das gilt für allerdings nur für VDD. Die I/O Pins können sogar nur 1.71 – 1.95 Volt vertragen (VDDIO). Bei einem 5 Volt Board ist das ein gewisses Problem, da ein HIGH Signal des ICM-20948 nicht mehr zuverlässig als HIGH-Signal erkannt wird. 

Ich habe das Problem gelöst, in dem ich einen Levelshifter eingesetzt und die Spannung auf der 3 Volt Seite zusätzlich mit 6.8 kΩ Widerständen heruntergezogen habe. Die Widerstandsgröße habe ich empirisch ermittelt. Zuvor habe ich über Wochen hinweg ohne zusätzliche Widerstände, also mit 3.3 Volt gearbeitet. Es ist mir kein ICM-20948 kaputt gegangen, aber es ist nun mal außerhalb der Spezifikation. 

Den Adresspin AD0 verbindet ihr mit HIGH (1.71-1.95 V) oder GND, je nachdem welche Adresse ihr verwenden wollt. In einigen Beispielsketchen verwende ich den Interruptpin INT, den ich an den Arduino Pin 2 gehängt habe.

Weitere Pins:

  • ADA und ACL sind Anschlüsse für weitere Sensoren, die über die I2C Hilfsschnittstelle angesteuert werden („A“ für auxiliary). Das habe ich nicht in allgemeiner Form implementiert, sondern nur für das Magnetometer.
  • NCS ist der Chip Select Pin für die Ansteuerung per SPI.
  • FYSYNC könnt ihr als externen Interruptpin einsetzen. Dazu habe ich später einen Beispielsketch.
Ansteuerung des ICM-20948 mit Arduino Nano und Level-Konverter
Ansteuerung des ICM-20948 mit Arduino Nano und Level-Konverter

Verbindung per SPI

Ich habe auch die schnellere Ansteuerung per SPI implementiert. Ein Beispielsketch findet ihr als Teil der Bibliothek.

SPI Ansteuerung des ICM-20948 mit Arduino Nano und Level-Konverter
SPI Ansteuerung des ICM-20948 mit Arduino Nano und Level-Konverter

Einführung in die ICM20948_WE Bibliothek

Ich habe einige Bibliotheken ausprobiert, konnte mich aber für keine so richtig begeistern. Deswegen habe ich meine eigene Bibliothek geschrieben, die ihr hier auf GitHub findet. Alternativ könnt ihr sie direkt über den Bibliotheksmanager der Arduino IDE installieren.

Das Problem mit dem ICM-20948 ist, dass er – bedingt durch seine 4 Sensoren plus I2C Hilfsschnittstelle – unglaublich viele Einstellmöglichkeiten hat. Und obwohl ich nicht alle Features implementiert habe, umfasst meine Bibliothek stolze neunundfünfzig öffentliche Funktionen. Um die Verwendung dieser Funktionen zu erklären, habe ich die Bibliothek mit dreizehn Beispielsketchen ausgestattet.

Ich gehe nun die Beispielsketche durch, was ziemlich trockene Lektüre ist. Vielleicht probiert ihr sie einfach aus und kommt hierher zurück, wenn ihr etwas nicht versteht.

Basisdaten mit dem ICM-20948 ermitteln

ICM20948_01_acceleration_data.ino

Auf diesen ersten Sketch gehe ich am intensivsten ein. Vieles wiederholt sich in den folgenden Sketchen.

Vorab noch eine Bemerkung zu dem Datentyp „xyzFloat“. Dabei handelt es sich um eine Struktur (struct), die aus drei Floatwerten besteht. Ich benutze xyzFloat für alle Daten, die eine x-, y- und z-Komponente haben.

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while(!Serial) {}
  
  if(!myIMU.init()){
    Serial.println("ICM20948 does not respond");
  }
  else{
    Serial.println("ICM20948 is connected");
  }

  /*  This is a method to calibrate. You have to determine the minimum and maximum 
   *  raw acceleration values of the axes determined in the range +/- 2 g. 
   *  You call the function as follows: setAccOffsets(xMin,xMax,yMin,yMax,zMin,zMax);
   *  The parameters are floats. 
   *  The calibration changes the slope / ratio of raw acceleration vs g. The zero point 
   *  is set as (min + max)/2.
   */
  myIMU.setAccOffsets(-16330.0, 16450.0, -16600.0, 16180.0, -16640.0, 16560.0);
    
  /* The starting point, if you position the ICM20948 flat, is not necessarily 0g/0g/1g for x/y/z. 
   * The autoOffset function measures offset. It assumes your ICM20948 is positioned flat with its 
   * x,y-plane. The more you deviate from this, the less accurate will be your results.
   * It overwrites the zero points of setAccOffsets, but keeps the the correction of the slope.
   * The function also measures the offset of the gyroscope data. The gyroscope offset does not   
   * depend on the positioning.
   * This function needs to be called after setAccOffsets but before other other settings since 
   * it will overwrite settings!
   */
  Serial.println("Position your ICM20948 flat and don't move it - calibrating...");
  delay(1000);
  myIMU.autoOffsets();
  Serial.println("Done!"); 
  
  /* enables or disables the acceleration sensor, default: enabled */
  // myIMU.enableAcc(true);

  /*  ICM20948_ACC_RANGE_2G      2 g   (default)
   *  ICM20948_ACC_RANGE_4G      4 g
   *  ICM20948_ACC_RANGE_8G      8 g   
   *  ICM20948_ACC_RANGE_16G    16 g
   */
  myIMU.setAccRange(ICM20948_ACC_RANGE_2G);
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off.  
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              246.0               1125/(1+ASRD) (default)
   *    1              246.0               1125/(1+ASRD)
   *    2              111.4               1125/(1+ASRD)
   *    3               50.4               1125/(1+ASRD)
   *    4               23.9               1125/(1+ASRD)
   *    5               11.5               1125/(1+ASRD)
   *    6                5.7               1125/(1+ASRD) 
   *    7              473.0               1125/(1+ASRD)
   *    OFF           1209.0               4500
   *    
   *    ASRD = Accelerometer Sample Rate Divider (0...4095)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setAccDLPF(ICM20948_DLPF_6);    
  
  /*  Acceleration sample rate divider divides the output rate of the accelerometer.
   *  Sample rate = Basic sample rate / (1 + divider) 
   *  It can only be applied if the corresponding DLPF is not off!
   *  Divider is a number 0...4095 (different range compared to gyroscope)
   *  If sample rates are set for the accelerometer and the gyroscope, the gyroscope
   *  sample rate has priority.
   */
  myIMU.setAccSampleRateDivider(10);
}

void loop() {
  myIMU.readSensor();
  xyzFloat accRaw = myIMU.getAccRawValues();
  xyzFloat corrAccRaw = myIMU.getCorrectedAccRawValues();
  xyzFloat gVal = myIMU.getGValues();
  float resultantG = myIMU.getResultantG(gVal);

   
  Serial.println("Raw acceleration values (x,y,z):");
  Serial.print(accRaw.x);
  Serial.print("   ");
  Serial.print(accRaw.y);
  Serial.print("   ");
  Serial.println(accRaw.z);

  Serial.println("Corrected raw acceleration values (x,y,z):");
  Serial.print(corrAccRaw.x);
  Serial.print("   ");
  Serial.print(corrAccRaw.y);
  Serial.print("   ");
  Serial.println(corrAccRaw.z);

  Serial.println("g-values (x,y,z):");
  Serial.print(gVal.x);
  Serial.print("   ");
  Serial.print(gVal.y);
  Serial.print("   ");
  Serial.println(gVal.z);

  Serial.print("Resultant g: ");
  Serial.println(resultantG);
  Serial.println("*************************************");
 
  delay(1000);
}

 

Initialisierung und Offsets

Ich habe mehrere Konstruktoren implementiert. Das erlaubt euch die I2C Adresse und/oder das I2C Objekt zu übergeben. Letzteres ist interessant, wenn ihr beispielsweise beide I2C Schnittstellen eines ESP32 nutzen wollt.

Die Funktion init() führt zunächst ein Reset des ICM-20948 durch und schreibt Defaultwerte in einige Register. init() liefert false zurück, wenn der ICM-20948 nicht ansprechbar sein sollte, ansonsten true.

Unbewegt wirkt nur die Erdbeschleunigung auf den ICM-20948. Wenn er dabei flach (also mit seiner x,y-Ebene) aufliegt, sollte der g-Wert für die x- und y-Achse null und für die z-Achse eins betragen. Diese Werte sind aber mehr oder weniger weit verschoben. Die Funktion autoOffsets() misst die Offsetwerte und zieht sie bei zukünftigen Messungen ab. Sie korrigiert allerdings nicht die Steigung.

autoOffsets() funktioniert nur dann vernünftig,

  • wenn das Modul flach mit seiner x,y-Ebene aufliegt,
  • nicht bewegt wird
  • und die Funktion im Setup als erste Funktion aufgerufen wird (da sie bestimmte Einstellungen verändert)

Alternativ könnt ihr die Funktion setAccOffsets() verwenden. Sie führt zu weniger guten Ergebnissen bzgl. des Nullpunkts, allerdings ist sie bei größeren Neigungen akkurater. Außerdem korrigiert sie die Steigung und ihr müsst das Modul bei Programmstart nicht ausrichten.

Weitere Einstellungen

Die Funktion setAccRange() legt den Messbereich für die Beschleunigungsmessung fest.

Um die Datenrate des Beschleunigungssensors zu steuern, verwendet ihr die Funktion setAccSampleRateDivider(divider).

\text{acceleration data rate}=\frac{1125}{1+divider}\;[\text{Hz}]\;\;\;\;\;\;\;\;\text{mit}\;\;divider = 0...4095

Das funktioniert aber nur, wenn der digitale Tiefpassfilter (digital low pass filter = DLPF) aktiviert ist. Mit setAccDLPF(level) schaltet ihr den DLPF ein und wählt die Stufe. Mit dem Parameter ICM20948_DLPF_OFF deaktiviert ihr den DLPF. Je höher die Stufe, desto geringer ist das Rauschen. Nur die Stufe 7 fällt diesbezüglich aus der Reihe. Der Nachteil einer hohen Glättung ist eine gewisse Trägheit. Das heißt, dass es bei Änderung der Beschleunigung eine gewisse Zeit dauert, bis ihr den korrekten Wert erreicht.

Mit enableAcc(true/false) schaltet Ihr den Beschleunigungssensor an bzw. aus.

Die Ergebnisse

Die Messergebnisse fragt ihr mit den folgenden Funktionen ab:

  • readSensor() liest die Datenregister im Burst Verfahren (also „in einem Rutsch“) aus und schreibt das Ergebnis in ein Puffer-Array.
    • Ohne vorherigen readSensor() Aufruf erhaltet ihr keine aktuellen Werte.
  • getAccRawValues() liefert die Rohwerte der Beschleunigung, so wie sie im ICM-20948 Datenregister stehen.
  • getCorrectedAccRawValues() liest die Rohwerte und korrigiert sie um die Offsets.
  • getGValues() liefert die Beschleunigungswerte in g (basierend auf den korrigierten Rohdaten).
  • getResultantG(gValue) berechnet die resultierende Beschleunigung aus einem g-Wertetripel, also den Betrag der Summe der drei Vektoren.
\text{resultantG}=\sqrt{(gValue.x)^2+(gValue.y)^2+(gValue.z)^2} \;\;\text{[g]}

Wirkt nur die Schwerkraft auf den ICM-20948, sollte die Resultierende immer 1 sein. Mit der Resultierenden könnt ihr bequem Beschleunigungen messen, ohne die Bewegung auf eine Achse ausrichten zu müssen. Zieht einfach 1 ab, um die Erdbeschleunigung herauszurechnen.

Die Ausgabe von ICM20948_01_acceleration_data.ino

ICM-20948 - Ausgabe von ICM20948_01_acceleration_data.ino
Ausgabe von ICM20948_01_acceleration_data.ino

ICM20948_02_gyroscope_data.ino

Da ihr nun schon einige Funktionen kennt, kann die Erklärung zu den Gyroskop Funktionen deutlich kürzer ausfallen.

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while(!Serial) {}
  
  if(!myIMU.init()){
    Serial.println("ICM20948 does not respond");
  }
  else{
    Serial.println("ICM20948 is connected");
  }

  Serial.println("Position your ICM20948 flat and don't move it - calibrating...");
  delay(1000);
  myIMU.autoOffsets();
  Serial.println("Done!"); 
  
  /*  The gyroscope data is not zero, even if you don't move the ICM20948. 
   *  To start at zero, you can apply offset values. These are the gyroscope raw values you obtain
   *  using the +/- 250 degrees/s range. 
   *  Use either autoOffsets or setGyrOffsets, not both.
   */
  //myIMU.setGyrOffsets(-115.0, 130.0, 105.0);
  
  /* enables or disables the gyroscope sensor, default: enabled */
  // myIMU.enableGyr(false);

  /*  ICM20948_GYRO_RANGE_250       250 degrees per second (default)
   *  ICM20948_GYRO_RANGE_500       500 degrees per second
   *  ICM20948_GYRO_RANGE_1000     1000 degrees per second
   *  ICM20948_GYRO_RANGE_2000     2000 degrees per second
   */
  myIMU.setGyrRange(ICM20948_GYRO_RANGE_250);
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off. 
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              196.6               1125/(1+GSRD) 
   *    1              151.8               1125/(1+GSRD)
   *    2              119.5               1125/(1+GSRD)
   *    3               51.2               1125/(1+GSRD)
   *    4               23.9               1125/(1+GSRD)
   *    5               11.6               1125/(1+GSRD)
   *    6                5.7               1125/(1+GSRD) 
   *    7              361.4               1125/(1+GSRD)
   *    OFF          12106.0               9000
   *    
   *    GSRD = Gyroscope Sample Rate Divider (0...255)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setGyrDLPF(ICM20948_DLPF_6);  
  
  /*  Gyroscope sample rate divider divides the output rate of the gyroscope.
   *  Sample rate = Basic sample rate / (1 + divider) 
   *  It can only be applied if the corresponding DLPF is not OFF!
   *  Divider is a number 0...255
   *  If sample rates are set for the accelerometer and the gyroscope, the gyroscope
   *  sample rate has priority.
   */
  //myIMU.setGyrSampleRateDivider(10);
}

void loop() {
  myIMU.readSensor();
  xyzFloat gyrRaw = myIMU.getCorrectedGyrRawValues();
  xyzFloat gyr = myIMU.getGyrValues();
    
  Serial.println("Raw gyroscope values (x,y,z):");
  Serial.print(gyrRaw.x);
  Serial.print("   ");
  Serial.print(gyrRaw.y);
  Serial.print("   ");
  Serial.println(gyrRaw.z);

  Serial.println("Gyroscope values (x,y,z):");
  Serial.print(gyr.x);
  Serial.print("   ");
  Serial.print(gyr.y);
  Serial.print("   ");
  Serial.println(gyr.z);
  Serial.println();

  delay(500);
}

 

Auch hier könnt ihr die autoOffsets() Funktion verwenden. Das Gyroskop sollte im nicht bewegten Zustand für alle Achsen den Messwert null liefern, oder zumindest um null herum schwanken. Ihr werdet aber einen gewissen (lageunabhängigen) Offset feststellen, den diese Funktion ermittelt. Alternativ setzt ihr die im Messbereich +/-250 °/s ermittelten Werte in setGyrOffsets() ein.

Den Tiefpassfilter (DLPF) aktiviert ihr mit setGyrDLPF(value) und legt die Stärke fest. Auch hier führt ein stärkerer Filter zu größeren Verzögerungen. Und ebenso fällt die Stufe 7 wieder aus der Reihe. Übergebt ihr als Parameter ICM20948_DLPF_OFF, schaltet ihr den DLPF damit ab.

Weitere Funktionen:

  • setGyrSampleRateDivider() funktioniert wie beim Beschleunigungssensor.
  • setGyrRange() legt den Messbereich fest.
  • enableGyr(true/false) aktiviert bzw. deaktiviert das Gyroskop.
  • getGyrRawValues() liefert die aktuell vorhandenen Rohdaten.
  • getCorrectedGyrRawValues() zieht die Offsets von den Rohdaten ab und liefert die korrigierten Daten.
  • getGyrValues() liefert die Gyroskop Daten in Grad/Sekunde, basierend auf den korrigierten Rohdaten.

Die Ausgabe von ICM20948_02_gyroscope_data.ino

ICM-20948 - Ausgabe von ICM20948_02_gyroscope_data.ino
Ausgabe von ICM20948_02_gyroscope_data.ino

ICM20948_03_magnetometer.ino

Das Magnetometer (AK09916) verhält sich wie ein separates Bauteil. Es hat eine eigene I2C Adresse, eigene Register und muss deswegen auch separat initialisiert werden. Wenn ihr einen I2C Scanner auf den ICM-20948 anwendet, werdet ihr aber nur seine Adresse sehen. Das Magnetometer liegt nämlich, nicht direkt zugänglich, sozusagen hinter dem ICM-20948.  Die Ansteuerung läuft über die oben beschriebene I2C Hilfsschnittstelle, die über den ICM-20948 eingerichtet wird. Davon bekommt ihr allerdings nichts mit, da die Bibliothek das im Hintergrund erledigt.

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while (!Serial) {}

  if (!myIMU.init()) {
    Serial.println("ICM20948 does not respond");
  }
  else {
    Serial.println("ICM20948 is connected");
  }

  if (!myIMU.initMagnetometer()) {
    Serial.println("Magnetometer does not respond");
  }
  else {
    Serial.println("Magnetometer is connected");
  }

  /* You can set the following modes for the magnetometer:
   * AK09916_PWR_DOWN          Power down to save energy
   * AK09916_TRIGGER_MODE      Measurements on request, a measurement is triggered by 
   *                           calling setMagOpMode(AK09916_TRIGGER_MODE)
   * AK09916_CONT_MODE_10HZ    Continuous measurements, 10 Hz rate
   * AK09916_CONT_MODE_20HZ    Continuous measurements, 20 Hz rate
   * AK09916_CONT_MODE_50HZ    Continuous measurements, 50 Hz rate
   * AK09916_CONT_MODE_100HZ   Continuous measurements, 100 Hz rate (default)
   */
  myIMU.setMagOpMode(AK09916_CONT_MODE_20HZ);
}

void loop() {
  myIMU.readSensor();
  xyzFloat magValue = myIMU.getMagValues(); // returns magnetic flux density [µT]

  Serial.println("Magnetometer Data in µTesla: ");
  Serial.print(magValue.x);
  Serial.print("   ");
  Serial.print(magValue.y);
  Serial.print("   ");
  Serial.println(magValue.z);

  delay(1000);
}

 

Einstellungen für das Magnetometer

Das Magnetometer hat einen Power-Down Modus, einen Trigger Modus und vier kontinuierliche Modi. Die kontinuierlichen Modi unterscheiden sich in der Datenrate, nämlich 10, 20, 40 oder 100 Hz. Ihr legt den Modus mit setMagOpMode() fest.

Die Messwerte erhaltet ihr mit getMagValues(). Das Ergebnis wird in Mikrotesla ausgegeben. Die Funktion liefert die Werte, die sich gerade im Datenpuffer befinden. Zuvor müsst ihr wieder mit readSensor() dafür sorgen, dass die Sensordaten aus den Datenspeichern gelesen werden.

Im Triggermodus löst setMagOpMode(AK09916_TRIGGER_MODE) eine Messung aus. Wenn ihr diese Option wählt, müsst ihr dem Magnetometer genügend Zeit lassen, die Messung durchzuführen und die Daten an den ICM-20948 zu übermitteln.

Die Ausgabe von ICM20948_03_magnetometer.ino

Für folgende Ausgabe habe ich den ICM-20948 flach in der x,y-Ebene gedreht:

ICM20948 - Ausgabe von ICM20948_03_magnetometer.ino
Ausgabe von ICM20948_03_magnetometer.ino

In Mitteleuropa liegt die magnetische Flussdichte des Erdmagnetfeldes bei ca. 20 µT in der Horizontalen und um 44 µT in der Vertikalen (Quelle: Wikipedia). Dementsprechend sollte die Differenz zwischen dem maximalen und dem minimalen Wert bei Drehung in der Horizontalen oder in der Vertikalen bei 40 beziehungsweise 88 µT liegen. Das werdet ihr wahrscheinlich so nicht messen, denn:

  • Es gibt auch hier Offsets.
  • Ihr messt wahrscheinlich drinnen, und je nach Bauweise des Gebäudes wird das Erdmagnetfeld abgeschirmt.
  • Wenn ihr auf dem Breadboard messt, können die Steckbrückenkabel und die Metallleitungen des Breadboards die Messungen beeinflussen.

ICM20948_04_calibration.ino

Dieser Sketch soll euch helfen, die Offsets für den Beschleunigungssensor und das Gyroskop zu ermitteln. Dazu wird zunächst der jeweils geringste Messbereich und der maximale Tiefpassfilter eingestellt. Das gewährleistet hohe Auflösung und geringes Rauschen.

Für die Offset-Ermittlung des Beschleunigungssensors dreht ihr den ICM-20948 langsam (!) um seine Achsen und notiert euch die maximalen und minimalen Rohwerte. Am besten stützt ihr dabei die Arme auf, denn jedes Zittern gibt zusätzliche Beschleunigung. Diese Daten benutzt ihr dann für die setAccOffsets() Funktion. Intern werden daraus die Offsets ermittelt. Wenn ihr den Messbereich wechselt, werden die Offsets automatisch angepasst. Mit dieser Methode seid ihr dann nicht mehr auf die ebene Ausrichtung des ICM-20948 bei Programmstart angewiesen. Für die Ermittlung präziser kleiner Neigungswinkel empfehle ich aber weiterhin die autoOffsets() Funktion.

Für die Gyroskop Offsets braucht ihr den ICM-20948 nicht zu drehen, denn der Offset ist unabhängig von der Neigung. Hier schreibt ihr die Ruhewerte für die x-, y- und z-Achse in die Funktion setGyrOffsets().

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while(!Serial) {}
  
  if(!myIMU.init()){
    Serial.println("ICM20948 does not respond");
  }
  else{
    Serial.println("ICM20948 is connected");
  }

  /*  This is a method to calibrate. You have to determine the minimum and maximum 
   *  raw acceleration values of the axes determined in the range +/- 2 g. 
   *  You call the function as follows: setAccOffsets(xMin,xMax,yMin,yMax,zMin,zMax);
   *  The parameters are floats. 
   *  The calibration changes the slope / ratio of raw accleration vs g. The zero point is 
   *  set as (min + max)/2.
   */
  myIMU.setAccOffsets(-16330.0, 16450.0, -16600.0, 16180.0, -16520.0, 16690.0);
    
  /*  The gyroscope data is not zero, even if you don't move the ICM20948. 
   *  To start at zero, you can apply offset values. These are the gyroscope raw values you obtain
   *  using the +/- 250 degrees/s range. 
   *  Use either autoOffset or setGyrOffsets, not both.
   */
  myIMU.setGyrOffsets(-115.0, 130.0, 105.0);
    
  /*  ICM20948_ACC_RANGE_2G      2 g   (default)
   *  ICM20948_ACC_RANGE_4G      4 g
   *  ICM20948_ACC_RANGE_8G      8 g   
   *  ICM20948_ACC_RANGE_16G    16 g
   */
  myIMU.setAccRange(ICM20948_ACC_RANGE_2G); // highest res for calibration
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off.  
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              246.0               1125/(1+ASRD) 
   *    1              246.0               1125/(1+ASRD)
   *    2              111.4               1125/(1+ASRD)
   *    3               50.4               1125/(1+ASRD)
   *    4               23.9               1125/(1+ASRD)
   *    5               11.5               1125/(1+ASRD)
   *    6                5.7               1125/(1+ASRD) 
   *    7              473.0               1125/(1+ASRD)
   *    OFF           1209.0               4500
   *    
   *    ASRD = Accelerometer Sample Rate Divider (0...4095)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setAccDLPF(ICM20948_DLPF_6); // lowest noise for calibration   
  
  /*  ICM20948_GYRO_RANGE_250       250 degrees per second (default)
   *  ICM20948_GYRO_RANGE_500       500 degrees per second
   *  ICM20948_GYRO_RANGE_1000     1000 degrees per second
   *  ICM20948_GYRO_RANGE_2000     2000 degrees per second
   */
  myIMU.setGyrRange(ICM20948_GYRO_RANGE_250); //highest resolution for calibration
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off. 
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              196.6               1125/(1+GSRD) 
   *    1              151.8               1125/(1+GSRD)
   *    2              119.5               1125/(1+GSRD)
   *    3               51.2               1125/(1+GSRD)
   *    4               23.9               1125/(1+GSRD)
   *    5               11.6               1125/(1+GSRD)
   *    6                5.7               1125/(1+GSRD) 
   *    7              361.4               1125/(1+GSRD)
   *    OFF          12106.0               9000
   *    
   *    GSRD = Gyroscope Sample Rate Divider (0...255)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setGyrDLPF(ICM20948_DLPF_6); // lowest noise for calibration
  
  /*  Choose a level for the Digital Low Pass Filter. 
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF          Bandwidth [Hz]      Output Rate [Hz]
   *    0             7932.0                    9
   *    1              217.9                 1125
   *    2              123.5                 1125
   *    3               65.9                 1125
   *    4               34.1                 1125
   *    5               17.3                 1125
   *    6                8.8                 1125
   *    7             7932.0                    9
   *                 
   *    
   *    GSRD = Gyroscope Sample Rate Divider (0...255)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setTempDLPF(ICM20948_DLPF_6); // lowest noise for calibration
}

void loop() {
  myIMU.readSensor();
  xyzFloat accRaw;
  xyzFloat gyrRaw;
  xyzFloat corrAccRaw;
  xyzFloat corrGyrRaw;
  accRaw = myIMU.getAccRawValues();
  gyrRaw = myIMU.getGyrRawValues();
  corrAccRaw = myIMU.getCorrectedAccRawValues();
  corrGyrRaw = myIMU.getCorrectedGyrRawValues();
  xyzFloat gVal = myIMU.getGValues();
  
  Serial.println("Acceleration raw values without offset:");
  Serial.print(accRaw.x);
  Serial.print("   ");
  Serial.print(accRaw.y);
  Serial.print("   ");
  Serial.println(accRaw.z);

  Serial.println("Gyroscope raw values without offset:");
  Serial.print(gyrRaw.x);
  Serial.print("   ");
  Serial.print(gyrRaw.y);
  Serial.print("   ");
  Serial.println(gyrRaw.z);

  Serial.println("Acceleration raw values with offset:");
  Serial.print(corrAccRaw.x);
  Serial.print("   ");
  Serial.print(corrAccRaw.y);
  Serial.print("   ");
  Serial.println(corrAccRaw.z);

  Serial.println("Gyroscope raw values with offset:");
  Serial.print(corrGyrRaw.x);
  Serial.print("   ");
  Serial.print(corrGyrRaw.y);
  Serial.print("   ");
  Serial.println(corrGyrRaw.z);

  Serial.println("g-values, based on corrected raws (x,y,z):");
  Serial.print(gVal.x);
  Serial.print("   ");
  Serial.print(gVal.y);
  Serial.print("   ");
  Serial.println(gVal.z);


  Serial.println("********************************************");

  delay(500);
}

 

Alles zusammen: ICM20948_05_acc_gyr_temp_mag_data.ino

Mithilfe dieses Sketches bestimmt ihr die Beschleunigung, die Gyroskopwerte, die Magnetometerwerte und die Temperatur. Die Temperatur ruft ihr mit getTemperature() ab, die anderen Funktionen habe ich schon erklärt. Ihr findet den Sketch in den Beispielen.

Das Thermometer dient weniger dazu, die Umgebungstemperatur zu messen, als die Temperaturentwicklung im ICM-20948 zu verfolgen. Sie liegt höher als die Raumtemperatur und steigt in Betrieb abhängig von den Messbedingungen.

Die Ausgabe von ICM20948_05_acc_gyr_temp_mag_data.ino

Die Ausgabe von ICM20948_05_acc_gyr_temp_mag_data.ino
Die Ausgabe von ICM20948_05_acc_gyr_temp_mag_data.ino

Winkel mit dem ICM-20948 messen

Ihr könnt die (Erd-)beschleunigungsdaten nutzen, um Neigungswinkel zu berechnen. Dabei müsst ihr sicherstellen, dass keine zusätzliche Beschleunigung auf den ICM-20948 wirkt. Ich habe dazu zwei Methoden implementiert.

ICM20948_06_angles_and_orientation.ino

Mit der in diesem Sketch vorgestellten Methode wird der Winkel α zwischen den Achsen und der Horizontalen ganz schlicht aus dem Arcussinus des Beschleunigungswertes berechnet.

\alpha=\arcsin(\text{g-Wert})

Für kleine Winkel funktioniert das einwandfrei, bei größeren Winkeln nimmt der Fehler zu. Warum das so ist, habe ich in meinem Beitrag über den MMA7361 erklärt. Bis 60° war die Abweichung bei meinen Versuchen kleiner als ein Grad.

Für diesen Sketch empfehle ich die autoOffset() Methode. Mithilfe dieser Funktion startet ihr mit einem Winkel von 0° für die x- und y-Achse. Für die z-Achse erhaltet ihr einen Startwert um 90°. Als großer Winkel schwankt der z-Achsen-Winkel recht stark. Für die Neigungswinkel erhaltet ihr nur dann vernünftige Werte, wenn ihr den ICM-20948 für die Kalibrierung flach positioniert.

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while(!Serial) {}
  
  if(!myIMU.init()){
    Serial.println("ICM20948 does not respond");
  }
  else{
    Serial.println("ICM20948 is connected");
  }

  /*  This is a method to calibrate. You have to determine the minimum and maximum 
   *  raw acceleration values of the axes determined in the range +/- 2 g. 
   *  You call the function as follows: setAccOffsets(xMin,xMax,yMin,yMax,zMin,zMax);
   *  The parameters are floats. 
   *  The calibration changes the slope / ratio of raw acceleration vs g. The zero point is 
   *  set as (min + max)/2.
   */
  myIMU.setAccOffsets(-16330.0, 16450.0, -16600.0, 16180.0, -16640.0, 16560.0);
    
  /* The starting point, if you position the ICM20948 flat, is not necessarily 0g/0g/1g for x/y/z. 
   * The autoOffset function measures offset. It assumes your ICM20948 is positioned flat with its 
   * x,y-plane. The more you deviate from this, the less accurate will be your results.
   * It overwrites the zero points of setAccOffsets, but keeps the the correction of the slope.
   * The function also measures the offset of the gyroscope data. The gyroscope offset does not   
   * depend on the positioning.
   * This function needs to be called after setAccOffsets but before other other settings since 
   * it will overwrite settings!
   */
  Serial.println("Position your ICM20948 flat and don't move it - calibrating...");
  delay(1000);
  myIMU.autoOffsets();
  Serial.println("Done!"); 
  
  /* enables or disables the acceleration sensor, default: enabled */
  // myIMU.enableAcc(true);

  /*  ICM20948_ACC_RANGE_2G      2 g   (default)
   *  ICM20948_ACC_RANGE_4G      4 g
   *  ICM20948_ACC_RANGE_8G      8 g   
   *  ICM20948_ACC_RANGE_16G    16 g
   */
  myIMU.setAccRange(ICM20948_ACC_RANGE_2G);
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off.  
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              246.0               1125/(1+ASRD) 
   *    1              246.0               1125/(1+ASRD)
   *    2              111.4               1125/(1+ASRD)
   *    3               50.4               1125/(1+ASRD)
   *    4               23.9               1125/(1+ASRD)
   *    5               11.5               1125/(1+ASRD)
   *    6                5.7               1125/(1+ASRD) 
   *    7              473.0               1125/(1+ASRD)
   *    OFF           1209.0               4500
   *    
   *    ASRD = Accelerometer Sample Rate Divider (0...4095)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setAccDLPF(ICM20948_DLPF_6);    
  
  /*  Acceleration sample rate divider divides the output rate of the accelerometer.
   *  Sample rate = Basic sample rate / (1 + divider) 
   *  It can only be applied if the corresponding DLPF is not off!
   *  Divider is a number 0...4095 (different range compared to gyroscope)
   *  If sample rates are set for the accelerometer and the gyroscope, the gyroscope
   *  sample rate has priority.
   */
  myIMU.setAccSampleRateDivider(10);
}

void loop() {
  myIMU.readSensor();
  
  xyzFloat gValue = myIMU.getGValues();
  xyzFloat angle = myIMU.getAngles();
  
/* For g-values the corrected raws are used */
  Serial.print("g-x      = ");
  Serial.print(gValue.x);
  Serial.print("  |  g-y      = ");
  Serial.print(gValue.y);
  Serial.print("  |  g-z      = ");
  Serial.println(gValue.z);

/* Angles are also based on the corrected raws. Angles are simply calculated by
   angle = arcsin(g Value) */
  Serial.print("Angle x  = ");
  Serial.print(angle.x);
  Serial.print("  |  Angle y  = ");
  Serial.print(angle.y);
  Serial.print("  |  Angle z  = ");
  Serial.println(angle.z);

  Serial.print("Orientation of the module: ");
  Serial.println(myIMU.getOrientationAsString());

  delay(1000);
}

 

Hier kommen nun folgende Funktionen hinzu:

  • getAngles() liefert den Winkel der x-, y- und z-Achse gegenüber der Horizontalen in Grad zurück.
    • g-Werte über 1 werden auf 1 beschnitten, da die Arcussinus Funktion nicht für größere Werte definiert ist.
  • getOrientationAsString() gibt an, welche Achse den größten positiven Winkel hat.
    • Mögliche Rückgabewerte sind: x up, x down, y up, y down, z up, z down.
    • Eine Alternative ist getOrientation(). Der Rückgabewert ist ein enum (ICM20948_orientation). Zur Definition schaut in ICM20948_WE.h.

Ausgabe von ICM20948_06_angles_and_orientation.ino

Für die folgende Ausgabe habe ich das Modul um die y-Achse gedreht, also die x-Achse aufgerichtet.

Ausgabe von ICM20948_06_angles_and_orientation.ino
Ausgabe von ICM20948_06_angles_and_orientation.ino

ICM20948_07_pitch_and_roll.ino

Bei dieser Methode werden für die Berechnung der Winkel mehrere Achsen herangezogen. Dadurch sind die Werte bei großen Winkeln genauer als bei der schlichten Arcussinus Methode. Hingegen ist bei kleinen Winkeln die letztere Methode vorzuziehen. Um die Methode abzugrenzen, habe ich mich der Nomenklatur anderer Bibliotheken bedient und den Neigungswinkel der x-Achse als „pitch“ (Steigungswinkel) und den der y-Achse als „roll“ (Rollwinkel) bezeichnet. Eine Definition findet ihr z.B. hier.

pitch\; angle= \arctan \left(\frac{-g_x}{\sqrt{g_y^2 +g_z^2}}\right)
roll\;angle = \arctan\left( \frac{g_y}{g_z} \right)

Zum Vergleich verwende ich in diesem Sketch beide Methoden. Probiert es aus und wählt, was euch mehr zusagt.

#include <Wire.h>
#include <ICM20948_WE.h>
#define ICM20948_ADDR 0x68

/* There are several ways to create your ICM20948 object:
 * ICM20948_WE myIMU = ICM20948_WE()              -> uses Wire / I2C Address = 0x68
 * ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR) -> uses Wire / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2)        -> uses the TwoWire object wire2 / ICM20948_ADDR
 * ICM20948_WE myIMU = ICM20948_WE(&wire2, ICM20948_ADDR) -> all together
 * Successfully tested with two I2C busses on an ESP32
 */
ICM20948_WE myIMU = ICM20948_WE(ICM20948_ADDR);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  while(!Serial) {}
  
  if(!myIMU.init()){
    Serial.println("ICM20948 does not respond");
  }
  else{
    Serial.println("ICM20948 is connected");
  }

  /*  This is a method to calibrate. You have to determine the minimum and maximum 
   *  raw acceleration values of the axes determined in the range +/- 2 g. 
   *  You call the function as follows: setAccOffsets(xMin,xMax,yMin,yMax,zMin,zMax);
   *  The parameters are floats. 
   *  The calibration changes the slope / ratio of raw acceleration vs g. The zero point 
   *  is set as (min + max)/2.
   */
  //myIMU.setAccOffsets(-16330.0, 16450.0, -16600.0, 16180.0, -16520.0, 16690.0);
    
  /* The starting point, if you position the ICM20948 flat, is not necessarily 0g/0g/1g for x/y/z. 
   * The autoOffset function measures offset. It assumes your ICM20948 is positioned flat with its 
   * x,y-plane. The more you deviate from this, the less accurate will be your results.
   * It overwrites the zero points of setAccOffsets, but keeps the the correction of the slope.
   * The function also measures the offset of the gyroscope data. The gyroscope offset does not   
   * depend on the positioning.
   * This function needs to be called after setAccsOffsets but before other other settings since it will 
   * overwrite settings!
   */
  Serial.println("Position your ICM20948 flat and don't move it - calibrating...");
  delay(1000);
  myIMU.autoOffsets();
  Serial.println("Done!"); 
  
  /* enables or disables the acceleration sensor, dafault: enabled */
  // myIMU.enableAcc(true);

  /*  ICM20948_ACC_RANGE_2G      2 g   (default)
   *  ICM20948_ACC_RANGE_4G      4 g
   *  ICM20948_ACC_RANGE_8G      8 g   
   *  ICM20948_ACC_RANGE_16G    16 g
   */
  myIMU.setAccRange(ICM20948_ACC_RANGE_2G);
  
  /*  Choose a level for the Digital Low Pass Filter or switch it off.  
   *  ICM20948_DLPF_0, ICM20948_DLPF_2, ...... ICM20948_DLPF_7, ICM20948_DLPF_OFF 
   *  
   *  DLPF       3dB Bandwidth [Hz]      Output Rate [Hz]
   *    0              246.0               1125/(1+ASRD) 
   *    1              246.0               1125/(1+ASRD)
   *    2              111.4               1125/(1+ASRD)
   *    3               50.4               1125/(1+ASRD)
   *    4               23.9               1125/(1+ASRD)
   *    5               11.5               1125/(1+ASRD)
   *    6                5.7               1125/(1+ASRD) 
   *    7              473.0               1125/(1+ASRD)
   *    OFF           1209.0               4500
   *    
   *    ASRD = Accelerometer Sample Rate Divider (0...4095)
   *    You achieve lowest noise using level 6  
   */
  myIMU.setAccDLPF(ICM20948_DLPF_6);    
  
  /*  Acceleration sample rate divider divides the output rate of the accelerometer.
   *  Sample rate = Basic sample rate / (1 + divider) 
   *  It can only be applied if the corresponding DLPF is not off!
   *  Divider is a number 0...4095 (different range compared to gyroscope)
   *  If sample rates are set for the accelerometer and the gyroscope, the gyroscope
   *  sample rate has priority.
   */
  //myIMU.setAccSampleRateDivider(10);
}

void loop() {
  myIMU.readSensor();
  xyzFloat gValue = myIMU.getGValues();
  xyzFloat angle = myIMU.getAngles();
  float pitch = myIMU.getPitch();
  float roll  = myIMU.getRoll();
 
  Serial.println("G values (x,y,z):");
  Serial.print(gValue.x);
  Serial.print("   ");
  Serial.print(gValue.y);
  Serial.print("   ");
  Serial.println(gValue.z);
  Serial.println("Angles (x,y,z):");
  Serial.print(angle.x);
  Serial.print("   ");
  Serial.print(angle.y);
  Serial.print("   ");
  Serial.println(angle.z);

  Serial.print("Pitch = "); 
  Serial.print(pitch); 
  Serial.print("  |  Roll = "); 
  Serial.println(roll); 
  
  Serial.println();
  Serial.println();

  delay(1000);
}

 

Zwei neue Funktionen werden hier verwendet:

  • getPitch() liefert den Pitch Winkel (x-Achse).
  • getRoll() liefert den Roll Winkel (y-Achse).

Ausgabe von ICM20948_07_pitch_and_roll.ino

ICM-20948 - Ausgabe von ICM20948_07_pitch_and_roll.ino
Ausgabe von ICM20948_07_pitch_and_roll.ino

Ausblick

Im zweiten Teil des Beitrages geht es dann um Interrupts, den Low-Power Modus und den FIFO Puffer.

27 thoughts on “ICM-20948 9-Achsensensor Teil I

  1. Danke für die schnelle Antwort.
    Ich hatte mich wohl etwas unvorteilhaft bei der Frage ausgedrückt, die Frage zielt auf den Pin OE ab. Im Datenblatt ist bei OE davon die Rede das dieser mit GND oder VA über einen Wiederstand Verbunden sein muss, nur mir ist aus dem Datenblatt nicht ersichtlich, was bei dieser Schaltung die richtige Wahl ist und irgendwo muss der TXS0108E sich vom TXB0108 ja unterscheiden.
    VG, Sebastian

    1. Beide scheinen dasselbe Verhalten sinsichtlich des OE Pins zu haben. Wenn du den OE Pin auf GND ziehst, dann gehen die Ausgänge in den High-Impedance State, d. h. sie sind sozusagen weder LOW noch HIGH, sondern neutral. Wo genau der Unterschied zwischen den beiden ist, das weiß ich nicht genau. ChatGBT sagt (aber das ist manchmal mit Vorsicht zu genießen!!):

      Der TXS0108E und der TXB0108 sind beide bidirektionale Pegelwandler-ICs (Level Shifters) von Texas Instruments, die verwendet werden, um Spannungspegel zwischen unterschiedlichen Logikfamilien (z. B. 1,8 V, 3,3 V und 5 V) zu übersetzen. Es gibt jedoch einige wesentliche Unterschiede zwischen ihnen, die sich auf ihre Einsatzmöglichkeiten auswirken:

      1. Anwendung und Betriebsweise:
      TXS0108E:

      Dieser Chip ist speziell für open-drain-Anwendungen ausgelegt. Das bedeutet, er ist besonders gut geeignet für I²C, SMBus und andere Protokolle, die offene Drain- oder offene Kollektor-Ausgänge verwenden.
      Er hat interne Pull-up-Widerstände, was ihn für solche Protokolle ohne externe Pull-ups nutzbar macht.
      Er unterstützt langsame und schnelle Signale, aber kann bei höheren Geschwindigkeiten begrenzt sein.
      TXB0108:

      Dieser Chip ist für allgemeine push-pull-Signale konzipiert. Er arbeitet effizient mit schnellen, bidirektionalen Signalen und kann höhere Frequenzen als der TXS0108E verarbeiten.
      Es ist jedoch wichtig zu beachten, dass er nicht für Protokolle mit offenen Drain (wie I²C) geeignet ist, da er keine internen Pull-ups hat und keine solche Art der Signalhandhabung unterstützt.
      2. Geschwindigkeit:
      TXS0108E: Aufgrund seiner internen Pull-ups und der Art, wie er Signale verarbeitet, ist er in der Geschwindigkeit begrenzt. Er funktioniert gut bei niedrigeren Frequenzen und bei Signalen, die sich langsamer ändern.
      TXB0108: Dieser Chip kann viel schneller arbeiten (bis zu 100 MHz bei niedrigen Pegeln). Er ist ideal für schnelle digitale Signale, die eine höhere Frequenz erfordern.
      3. Eingangskapazität und Belastung:
      TXS0108E: Dieser Chip ist robuster in Bezug auf kapazitive Lasten, was ihn in einigen Fällen stabiler macht, insbesondere bei längeren Leitungen oder Leitungen mit höherer Kapazität.
      TXB0108: Bei höheren kapazitiven Lasten kann dieser Chip an seine Grenzen stoßen. Er arbeitet am besten mit minimaler kapazitiver Last und kurzen Leitungen.
      4. Stromaufnahme:
      TXS0108E: Aufgrund der Pull-up-Widerstände und der Art der Pegelübersetzung kann er eine etwas höhere Stromaufnahme haben, besonders wenn mehrere Leitungen gleichzeitig aktiv sind.
      TXB0108: Er hat eine geringere Stromaufnahme bei gleichzeitiger Fähigkeit, hohe Geschwindigkeiten zu verarbeiten.
      5. Empfohlene Verwendung:
      TXS0108E: Ideal für langsame Protokolle wie I²C und andere offene Drain-Protokolle.
      TXB0108: Besser geeignet für schnelle, bidirektionale digitale Signale wie SPI oder UART.
      Zusammengefasst: Der TXS0108E ist besser für langsame, offene Drain-Schnittstellen wie I²C, während der TXB0108 für schnelle push-pull-Schnittstellen gedacht ist.

  2. Hallo,
    lässt sich der in dem Diagramm zur SPI Verkabelung abgebildete TXB0108 Level Shifter ohne weiteres durch den TXS0108E Level Shifter ersetzen, oder muss etwas bei der Verkabelung abgeändert werden?

    1. Hallo, ich denke die Frage zieht vor allem auf die zusätzlichen Widerstände auf der ICM20948-Seite ab, richtig? Wenn ich die Datenblätter richtig interpretiere, dann haben beide einen 4 kOhm Widerstand auf dem Niedrigspannungsausgang. Insofern solltest du bei beiden mit dieselben Widerstände zur Spannungsteilung einsetzen. Ich würde aber mal mit einem Voltmeter sicherheitshalber nachmessen.

      VG, Wolfgang

  3. Hallo Wolfgang,

    ich teste gerade ICM20948_06_angles_and_orientation.ino. Die Werte der Winkel für x und y sind ok. Der Winkel z hat immer den Wert 90 Grad bei Drehungen nach links oder rechts.

    g-x = -0.00 | g-y = 0.00 | g-z = 1.00
    Angle x = -0.01 | Angle y = 0.11 | Angle z = 90.00
    Orientation of the module: z up

    Wie kann ich mir den Winkel z bei Drehbewegungen anzeigen lassen?

    Viele Grüße
    Ferdi

    1. Hi Ferdi,

      was links oder rechts ist, hängt vom Betrachtungswinkel ab. Wenn die z-Achse senkrecht ausgerichtet ist (Modul liegt flach) und du guckst von oben, dann sollte eine Links- oder Rechtsdrehung nichts an den Winkeln ändern. Neigst du die y-Achse, dann sollte sich nur der y- und z-g-Wert und Winkel ändern. Bei Neigen der x-Achse solltest du die Änderungen bei x und z feststellen. Bei einem leichten Neigen kann es sein, dass z bei 90° bleibt, da die Messungen um 90° herum relativ ungenau sind. Genauer gesagt, die Änderung von g mit dem Winkel ist kleiner als um 0° herum. Tut sich denn gar nichts am z-Winkel? Wenn nein, dann ist entweder das Modul defekt oder du benutzt die automatische Kalibrierung in einer dafür nicht vorgesehenen Lage (das Modul muss flachliegen). Du könntest mal die Kalibrierung herauskommentieren und schauen was passiert.

      VG, Wolfgang

  4. Hi. Ich habe vor paar Monaten schon mal auf der Englischen Version der Website geschrieben, und habe mittlerweile die erste Platine mit dem ICM20948 bestellt und zusammengelötet.
    An sich funktioniert auf der Platine alles, mit einem I2C Scan bekomme ich die Adresse des EEPROMS und die vom ICM20948. Beide Teile lassen sich auch initialisen allerdings habe ich vom ICM20948 bis jetzt keine daten bekommen können, ich bekomme im seriellen monitor immer nur „[ 4373][E][Wire.cpp:499] requestFrom(): i2cWriteReadNonStop returned Error -1“ wenn ich den chip auslesen will.
    Mein setup ist:
    ESP32 C3 (GPIO9=SDA, GPIO10=SCL) -> TCA9406YZPR -> ICM20948
    Ich habe viele verschiedene Sketches versucht aber immer den selben Fehler gehabt, hier beziehe ich mich also auf den „ICM20948_05_acc_gyr_temp_mag_data“ Sketch den ich bisschen modifiziert habe, dass er printf() nutzt.
    Ich könnte noch die Schematic/Gerber Files und die daten von einem Logic-Analyzer schicken, weis allerdings nicht wie ich das hier anhänge.
    Wir könnten gerne auch über Discord kommunizieren, da antworte ich wahrscheinlich direkt 🙂
    Grüße Jacob

    1. Hallo, hast denn mal den ESP32 C3 direkt, also ohne den EEPROM und ohne den TCA9406 verbunden? Wenn etwas nicht geht, dann versuche ich erst einmal zu vereinfachen, bis es geht, denn dann weiß ich, was oder welche Kombination nicht geht. Das wäre mit Abstand die größte Hilfe. Ich möchte nicht ausschließen, dass es Boards gibt, für die ich vielleicht noch eine Anpassung implementieren muss. Es ist immer schwer, wenn schon mehrere Komponenten im Spiel sind, weil das die Fehlermöglichkeiten potenziert. Discord nutze ich nicht, aber du kannst mir deine Schaltung und den Sketch mal per guter, alter E-Mail schicken (wolfgang.ewald@wolles-elektronikkiste.de). Eine Schaltung ist hilfreich, da muss ich aber mal schauen, welches Format ich lesen kann. In den Logic Analyzer Daten herumzusuchen, da fehlt mir wahrscheinlich die Zeit. Eins nach dem anderen. VG, Wolfgang

  5. Hallo Wolfgang, du hast Pull Downs hinter deinem Level Shifter. Ich verstehe nicht zu 100% was du da getan hast. Du hast da ja keinen wirklichen Spannungsteiler, abgesehen von dem Widerstand des Pegelwandlers. Mein Pegelwandler regelt meine 3.3V VCC auf 1.8V direkt. Daher benötige ich die Pull Downs nicht, denke ich. Natürlich könnten Pull Downs die Signalintegrität verbessern. Ich habe gerade bei Megahertzfrequenzen einen negativen Gleichstromanteil festgestellt.

    1. Hallo, wenn du dir das Blockdiagramm im Datenblatt des TXB0108 anschaust, wirst du sehen, dass das Teil an den Low-Voltage Ausgängen 4 kOhm Widerstände in Serie hat. Zusammen mit den 6.8 kOhm Widerständen bilden sie einen Spannungsteiler. Rechnerisch ergibt das 2.07 Volt, praktisch war lag ich damit im korrekten Bereich. Wenn dein Level-Shifter 1.8 Volt darstellen kann, dann braucht du dich darum nicht zu kümmern.

  6. Hallo Wolfgang,
    erstmal finde ich es klasse wie du alles aufgebaut hast und alles verständlich erklärst.
    Ich habe deine Beispielsketche 01_acceleration_data, 04_calibration und 05_acc_gyr_temp_mag_data getestet, aber kein Sketch funktioniert bei mir. Ich verwende einen ESP32 und habe den Sensor mit I2C angeschlossen und anscheinend will der SPI. Wie kann/soll ich das umstellen?

    Folgendes wird im Monitor angezeigt:
    21:52:24.444 -> rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    21:52:24.444 -> configsip: 0, SPIWP:0xee
    21:52:24.444 -> clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
    21:52:24.444 -> mode:DIO, clock div:1
    21:52:24.444 -> load:0x3fff0018,len:4
    21:52:24.444 -> load:0x3fff001c,len:1216
    21:52:24.444 -> ho 0 tail 12 room 4
    21:52:24.444 -> load:0x40078000,len:10944
    21:52:24.444 -> load:0x40080400,len:6388
    21:52:24.444 -> entry 0x400806b4
    21:52:24.580 -> ICM20948 does not respond
    21:52:24.580 -> Position your ICM20948 flat and don’t move it – calibrating…
    21:52:26.203 -> Done!
    21:52:26.250 -> Raw acceleration values (x,y,z):
    21:52:26.250 -> 0.00 0.00 0.00
    21:52:26.250 -> Corrected raw acceleration values (x,y,z):
    21:52:26.250 -> nan nan inf
    21:52:26.250 -> g-values (x,y,z):
    21:52:26.250 -> nan nan inf
    21:52:26.250 -> Resultant g: nan
    21:52:26.250 -> *************************************

    Viele Grüße
    Christian

    1. Hallo Christian,
      ich habe es gerade noch einmal probiert und es funktioniert. Die Meldung „rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)“ hat noch nichts mit dem Sketch zu tun, das sind Bootmeldungen und die sind bei mir dieselben. Das einzige was ich festgestellt habe ist, dass, wenn man einen Sketch hochgeladen hat, den ESP32 trennt, ihn dann wieder an den den Computer anschließt und dann den seriellen Monitor öffnet, man den ESP32 resetten muss. Und das lässt sich mit einem delay(2000); nach dem while(!Serial) {} verhindern. Geht es bei dir, wenn du noch einmal resettest?
      Wenn nicht, dann würde ich als nächstes einen I2C Scanner Sketch laufen lassen und schauen ob die I2C Adresse erkannt wird. Gibt’s z.B. hier:
      https://wolles-elektronikkiste.de/i2c-scanner
      Wenn keine Adresse erkannt wird, dann würde ich noch einmal die Verkabelung überprüfen. Ist die Stromversorgung OK? Vielleicht mal die Kabel tauschen. Ist AD0 an GND? Hast du die richtigen SDA/SCL Pins gewählt, nämlich GPIO 21 und 22?
      VG, Wolfgang

      1. Hallo Wolfgang,
        ich habe dank dem I2C Scanner den Fehler gefunden. Ich musste die I2C Adresse ändern, von #define ICM20948_ADDR 0x68 zu #define ICM20948_ADDR 0x69.
        Ich habe auch nochmal in die Adafruit_ICM20948.h Datei geschaut und da steht auch: #define ICM20948_I2CADDR_DEFAULT 0x69 ///< ICM20948 default i2c address

        Viele Grüße
        Christian

  7. Hallo Wolfgang,

    vielen Dank für diese ausführliche Beschreibung und die Bibliothek.
    Ich habe das Beispiel ICM20948_03_magnetometer.ino ausprobiert und sehe große Schwankungen in den Messwerten des Magnetometer. Die Messwerte schwanken um bis zu 5uT.
    Das Board liegt ruhig und flach auf dem Boden mit möglichst wenig Störeinflüssen.
    Ich benutze SPI.
    Kannst du das Verhalten bestätigen? Und gibt es dafür ein Lösung?
    Vielen Dank im Voraus,
    Marten

    1. Hallo Marten ich kann eine Schwankung von ca. +/- 2 µT um einen Mittelwert bestätigen. Das Magnetometer hat keine Dämpfungsfunktion, wie es sie für das Accelerometer und das Gyroskop gibt. Da hilft aus meiner Sicht nur, die Werte zu mitteln oder eine eigene Dämpfungsfunktion zu schreiben, z.B. in der Art:
      magValue.x = 0.2 * magValue.x + 0.8 * formerX;
      formerX = magValue.x;
      Dann muss man natürlich etwas öfter abfragen, damit man nicht zu viel Verzögerung reinbekommt.
      VG, Wolfgang

      1. Vielen Dank für die sehr schnelle Antwort.
        Ich möchte die Orientierung bestimmen. Selbst mit Dämpfungsfunktion schwankt der Winkel um bis zu +/- 1,5 Grad. Das ist zuviel für meine Anwendung. Ich möchte die Orientierung auch möglichst schnell bestimmen. Eine größere Dämpfung würde wiederum zu lange dauern.
        Zumindest kann ich nun einen Hardwarefehler ausschließen.
        Der Messbereich von +/-4900uT ist auch irgendwie nicht geeignet um das Erdmagnetfeld zu messen (30-50uT). Ich werde mich wohl nach einem anderen Sensor umsehen müssen.

  8. Hallo,
    erstmal vielen Dank für den Tollen Blog.
    Ich habe mir gerade deine Biblothek angeschaut und frage mich wie du auf die 16384 gekommen bist.
    gVal.x = accRawVal.x * accRangeFactor / 16384.0;

    Vielen Dank,

    Jonas

    1. Hallo Jonas,
      die Register für die rohen Beschleunigungswerte umfassen 16 Bit. Da die Werte vorzeichenbehaftet sind, gehen sie also von -2^15 bis +2^15. Wenn du die Range +/-2g wählst, entsprechen -2g dem Rohwert -2^15 und +2g entsprechen +2^15. 1g entspricht also 2^15/2 = 32768/2 = 16384. Der accRangeFactor ist 1 für die Range +/-2g. Wenn du die Range wechselst, muss das in der Berechnung berücksichtigt werden. Für +/-4g ist der Faktor 2, für +/-8g ist er 4 und für +/-16 ist er 8. Also nix Geheimnisvolles!
      VG, Wolfgang

  9. Hallo Wolfgang,
    da hast du echt eine super Arbeit vollbracht.
    Ich benutze deine Werte in dieser „LIB“ https://github.com/aster94/SensorFusion
    Die soll da eigentlich Pitch Roll und Yaw ausgeben.
    Macht sie auch aber die Werte laufen Quasi immer wieder von 0 bis zum Maximum.
    Das knüpft auch an die Frage von Moe an.

    Vielleicht magst du dir das ja mal anschauen und findest den Fehler.
    Diese Sensor Fusion ist echt eine heikle Sache. Ich haben da schon unendlich viele Stunden zugebracht.

    Grüße Jörg

    1. Hallo Jörg,
      vielen Dank, ich schaue mir das gerne an. Ist aber sicherlich nichts „für mal eben“…
      VG, Wolfgang

      1. Hallo Wolfgang, Bei Magnetometer ist die Z und Y Achse laut Datenblatt zum Gyro und Accelerator invertiert. Vielleicht ist das ein Problem. Wie ließe sich das programmiertechnisch invertieren?

        MfG

        Jörg Boge

        1. Hallo Jörg, das in der Bibliothek umzudrehen, wäre kein Problem und in wenigen Minuten erledigt. Du könntest das erstmal einfach testen, indem du nach der Abfrage der Magnetometerwerte die Achsen änderst, z.B. mit der folgenden Funktion:
          xyzFloat changeMagAxes(xyzFloat value){
          float intermediateVal = value.y;
          value.y = value.z;
          value.z = intermediateVal;
          return value;
          }

          Aufruf:
          magValue = changeMagAxes(magValue);

          Das muss natürlich nach jeder Abfrage erneut geschehen.

          Wenn es nicht geht, dann könnte man noch einmal testen, ob nur die Achsen vertauscht sind, oder auch die Richtungen. Also z.B.: value.z = – value.y;

          Und wie gesagt, kein Problem, das in der Bibliothek zu ändern.
          VG, Wolfgang

    2. Hallo Wolfgang,
      ich hatte es erst mit der MPU 9250, allerdings mit der Bolder Flight Lib, getestet Da waren die Werte nach ca. 30 Sekunden relativ Stabil.
      Gruß Jörg

  10. Vielen Dank für diese übersichtliche Zusammenfassung!

    Ist der Betrieb mit einem 3,3V Controller ohne „Level Schifter“ wirklich möglich? Wenn ich das Datenblatt richtig deute, darf Vddio nur 1,95V haben was wiederum den I2C auch auf diese Spannung begrenzt.
    Ich würde mich um Aufklärung freuen.
    Vielen Dank!
    Gruß Holgus

    1. Danke für den Hinweis, du hast Recht! Da muss ich nochmal drauf hinweisen und einen zusätzlichen Teiler in die Schaltung einbauen. Ein paar wenige Module, wie das von Adafruit, haben entsprechende Teiler integriert, das oben abgebildete nicht. VG, Wolfgang

      Update: Ich habe die Spannung der SDA und SCL Leitungen mit weiteren Widerständen heruntergezogen und den Text etwas umgeschrieben. Danke nochmal.

  11. Genial DANKE für die Bibliothek sobald der Sensor eintrifft wird der Ausgecheckt..

    Was kann der DMP denn ? Bewegungsmuster speichern und erkennen?

    Wenn ich auf den Tisch schlage, kann ich dann die Schwingungen messen wie der Tisch schwingt?

    Gruß Moe

    1. Hi Moe,

      in irgendeiner Form werden die Daten der drei Sensoren kombiniert. Damit lässt sich dann die Lage im Raum besser bestimmen. Offen gestanden habe ich das nicht vollständig verstanden. Die Sparkfun Bibliothek hat das zumindest teilweise implementiert. Hier ist eine Zusammenfassung:

      https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/blob/main/DMP.md

      Alles klar? Mir nicht! Nachdem ich mich ein paar Stunden damit beschäftigt habe, habe ich für mich das Fazit gezogen, dass mich das zuviel Zeit kosten würde es vollständig zu verstehen und zu implementieren.

      VG, Wolfgang

Schreibe einen Kommentar

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