Trouble trying to get sensor fusion to work with ICM 20948 and Arduino Uno

I bought one of these IMU breakboard boards: https://www.sparkfun.com/products/15335

I’ve got it talking to an Arduion Uno over I2C and reading data using this library: https://github.com/sparkfun/SparkFun_IC … inoLibrary

Unfortunately I cannot access the DMP on the chip to do sensor fusion computations (due to memory limits), so I need to do those computations on the Arduino. I tried applying MadgwickAHRS to the data read from the IMU, but I’m not having good results with orientation (e.g. lots of drift, I rotate it 90 deg, but it only updates about 45 deg in my visualization application). Are there some other techniques I can apply to get an acceptable orientation computation?

Here is my code thus far:

// This is meant to be nothing more than a sketch that dumps data on the serial port
// for the python application to show/visualize.
// Communication protocol:  This print strings on the Serial port, at the rate of 115200 baud; it never reads data
//   It might print out status messages like "waiting for data"
//   Actual orientation data will follow the format:
//
//     HPR <heading/yaw deg> <pitch deg> <roll deg>
//
//   For example:
//
//     HPR 5.5 -10.07773 0.22000023
//

#include <Arduino.h>
#include <Wire.h>
#include <ICM_20948.h>
#include "MadgwickAHRS.h"
//#include "MahonyAHRS.h"


#define SERIAL_BAUD_RATE 115200
#define I2C_RATE 400000
#define IMU_TIMEOUT_MS 500                                                  // When there was a failure in talking to the IMU (or no data ready), how many milliseconds should we wait?
#define IMU_DATA_READING_WAIT_MS 5                                         // How many milliseconds to wait before attempting the next data reading
#define IMU_FUSION_FREQUENCY (1000.0f / float(IMU_DATA_READING_WAIT_MS))    // The sensor fusion algorithm needs to know how often the IMU is being read from
#define SERIAL_PRECISION 12                                                 // How many digits of the float to print to Serial
#define SERIAL_NUM_UPDATES_PER_SECOND 20                                    // How many times per second to send an update

// TODO research this better
#define AD0_VAL   1     // The value of the last bit of the I2C address.
                        // On the SparkFun 9DoF IMU breakout the default is 1, and when
                        // the ADR jumper is closed the value becomes 0

// Using I2C to connect to the imu
ICM_20948_I2C imu;

// For orientation calculation
Madgwick filter;
//Mahony filter;
float heading, pitch, roll;     // The orientation

// For sending orientation data to the computer
#define SERIAL_UPDATE_DELTA_MS (1000 / SERIAL_NUM_UPDATES_PER_SECOND)       // How many milliseoncds to wait until to update the orientation on the serial connection
unsigned long prev_serial_update_ms = 0;                                    // Previous time (in milliseconds) and update was sent


void setup() {
    // Setup communications
    Serial.begin(SERIAL_BAUD_RATE);     // To computer
    Wire.begin();                       // To IMU
    Wire.setClock(I2C_RATE);

//    imu.enableDebugging();

    // Try to enable the IMU
    bool connected = false;
    while (!connected) {
        imu.begin(Wire, AD0_VAL);

        // Print status to serial
        Serial.print("Init connection to ICM 20948: ");
        Serial.println(imu.statusString());

        // If it didn't work, try again shortly
        if (imu.status != ICM_20948_Stat_Ok) {
            Serial.println("Try again in 500 ms");
            delay(IMU_TIMEOUT_MS);
        } else
            connected = true;
    }

    Serial.println("Connected to ICM 20948");

    // Setup filter
    filter.begin(IMU_FUSION_FREQUENCY);
}


void loop() {
    unsigned long current_millis = millis();

    // If enough time has passed, send an update of the orientation over the serial connection
    if (current_millis >= (prev_serial_update_ms + SERIAL_UPDATE_DELTA_MS)) {
        // Mark the time we send the data over
        prev_serial_update_ms = current_millis;

        // Send Orientation data to the PC
        Serial.print("HPR ");                       // HPR = Heading, Pitch, Roll
        Serial.print(heading, SERIAL_PRECISION);
        Serial.print(" ");
        Serial.print(pitch, SERIAL_PRECISION);
        Serial.print(" ");
        Serial.print(roll, SERIAL_PRECISION);
        Serial.println("");     // End the line reading
    }

    if (imu.dataReady()) {
        // Some data is ready
        imu.getAGMT();          // Retrieves data

        // Update the sensor fusion state
        filter.update(
            imu.gyrX(), imu.gyrY(), imu.gyrZ(),     // These units might be in degrees and not radiasn
            imu.accX(), imu.accY(), imu.accZ(),
            imu.magX(), imu.magY(), imu.magZ()
        );

        // Read data
        heading = filter.getYaw();
        pitch = filter.getPitch();
        roll = filter.getRoll();

        // Wait a little before the next reading
        delay(IMU_DATA_READING_WAIT_MS);
    } else {
        // Nothing, pause for a moment
 //       Serial.println("Waiting for data...");
        delay(IMU_TIMEOUT_MS);
    }
}

Here is the visitation app that runs on my laptop. You can see there is some constant drifting involved. At the end I’m not touching the board no longer

https://i.imgur.com/sBwHDco.gif

To be functional, the Madgwick and the Mahony filters require that the magnetometer, gyro and accelerometer use the same axial orientations (the sensor does not, so that has to be corrected), that you calibrate the gyro and at least the magnetometer, and that the gyro rates be in radians/second.

What would be the proper calibration procedure? Could you give me the pseudo code for it, or is there something in the datasheet of the ICM 20948 or SparkFun example code that I missed?

As for Madgwick/Mahoney requiring that gyro data be in radians/sec, I read into the source of the library and here, a comment says that the data is being converted from deg/sec to rad/sec. Which leaves me to believe that the MadgwickAHRS library requires the gyro input to be deg/sec:

https://github.com/arduino-libraries/Ma … RS.cpp#L61

See below.

The Madgwick and Madgwick/Mahony filters require the rate gyro data to be in radians/sec. If they aren’t, the filter won’t work, so double check your conversion, which depends on the gyro sensitivity setting.

Comprehensive tutorial on calibration: https://thecavepearlproject.org/2015/05 … r-arduino/ A detailed example is posted here: https://forum.pololu.com/t/correcting-t … r/14315/12

The ICM 20948 data sheet makes it pretty clear that the signs of the magnetometer data need to be flipped, or the filter will return meaningless angles. Check your work by taking the setup outside and determine whether it knows the direction of magnetic North, and that the tilt and roll angles behave as expected.

and that the tilt and roll angles behave as expected.

This is something odd that I have noticed, Say if the IMU’s breakout board is pointing forward (Gyro/Accel Y+), If I rotate the breakout board about the Z axis it rotates as I expect it. But If I try to rotate the breakout board forward about the X axis (gyro/Accel X+), it’s being rolled to the left (across Y+ axis).

This is currently my code. I increased the range of sensitivity from 2g to 16g, and the dps from 250 to 2000. It does seem to operate much better, but there’s still drift.

// This is meant to be nothing more than a sketch that dumps data on the serial port
// for the python application to show/visualize.
//
// For the actual  application:
//   - This should use the DMP on the chip for Sensor fusion instead of the Madgwick code
//     - Probably can't use Arduino then anymore, since the Sparkfun code breaks the Memory limits of the Arduino UNO chip
//   - Gyroscope sensitivity probably needs to be adjusted to be a little less sensitive (giving a greater DPS range)
//   - Should properly use the interrupt pin
//
// Communication protocol:  This print strings on the Serial port, at the rate of 115200 baud; it never reads data
//   It might print out status messages like "waiting for data"
//   Actual orientation data will follow the format:
//
//     HPR <heading/yaw deg> <pitch deg> <roll deg>
//
//   For example:
//
//     HPR 5.5 -10.07773 0.22000023
//

#include <Arduino.h>
#include <Wire.h>A
#include <ICM_20948.h>
#include "MadgwickAHRS.h"
//#include "MahonyAHRS.h"


#define SERIAL_BAUD_RATE 115200
#define I2C_RATE 400000
#define IMU_TIMEOUT_MS 500                                                  // When there was a failure in talking to the IMU (or no data ready), how many milliseconds should we wait?
#define FUSION_UPDATES_PER_SECOND 50.0f                                    // Rate to update the fusion algorithm
#define FUSION_UPDATE_DELTA_US static_cast<unsigned long>(1000000.0f / FUSION_UPDATES_PER_SECOND)        // How many milliseconds need to pass before the fusion algoirthm is sent an update
#define SERIAL_PRECISION 12                                                 // How many digits of the float to print to Serial
#define SERIAL_NUM_UPDATES_PER_SECOND 20                                    // How many times per second to send an update
#define SERIAL_UPDATE_DELTA_MS (1000 / SERIAL_NUM_UPDATES_PER_SECOND)       // How many milliseoncds to wait until to update the orientation on the serial connection

// TODO research this better
#define AD0_VAL   1     // The value of the last bit of the I2C address.
                        // On the SparkFun 9DoF IMU breakout the default is 1, and when
                        // the ADR jumper is closed the value becomes 0

// Using I2C to connect to the imu
ICM_20948_I2C imu;

// For orientation calculation
Madgwick filter;
//Mahony filter;

// Data readings (TODO units)
float ax = 0.0f, ay = 0.0f, az = 0.0f;      // Accelration
float gx = 0.0f, gy = 0.0f, gz = 0.0f;      // Gyroscope
float mx = 0.0f, my = 0.0f, mz = 0.0f;      // Magnometer

// The orientation (in degrees)
float heading = 0.0f, pitch = 0.0f, roll = 0.0f;

// For updating certain things
unsigned long prev_serial_update_ms = 0;                                    // Previous time (in milliseconds) and update was sent
unsigned long prev_fusion_update_us = 0;                                    // Previous time (in microseonds) the fusion algorithm was updated


// converts degrees to radians
//   x - an angle in degrees
//   returns: the same angle, but expressed in radians
inline float deg_to_rad(const float x) {
    return x * (3.1415926535f / 180.0f);
}

void setup() {
    // Setup communications
    Serial.begin(SERIAL_BAUD_RATE);     // To computer
    Wire.begin();                       // To IMU
    Wire.setClock(I2C_RATE);

//    imu.enableDebugging();

    // Try to enable the IMU
    bool connected = false;
    while (!connected) {
        imu.begin(Wire, AD0_VAL);

        // Print status to serial
        Serial.print("Init connection to ICM 20948: ");
        Serial.println(imu.statusString());

        // If it didn't work, try again shortly
        if (imu.status != ICM_20948_Stat_Ok) {
            Serial.println("Try again in 500 ms");
            delay(IMU_TIMEOUT_MS);
        } else
            connected = true;
    }

    Serial.println("Connected to ICM 20948");

    // Do a reset to make sure device is in a known state
    imu.swReset();
    if (imu.status != ICM_20948_Stat_Ok) {
        Serial.print("Error resetting the IMU: ");
        Serial.println(imu.statusString());
    }
    delay(250);     // Wait 1/4 second

    // Wake up the IMU
    imu.sleep(false);
    imu.lowPower(false);

    // Change the range of values collected to the higest possible
    // as people might be jerking this around quite a bit
    ICM_20948_fss_t fss;
    fss.a = gpm16;           // Accelerometer 16 g of gravity
    fss.g = dps2000;         // Gyroscope 2000 deg/sec

    imu.setFullScale((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), fss);
    if (imu.status != ICM_20948_Stat_Ok) {
        Serial.print("Error setting IMU collection range: ");
        Serial.println(imu.statusString());
    }
//    delay(250);     // Wait 1/4 second

/*
    // Add digital low-pass filter
    ICM_20948_dlpcfg_t dlp_cfg;
    dlp_cfg.a = acc_d473bw_n499bw;
    dlp_cfg.g = gyr_d361bw4_n376bw5;
    imu.setDLPFcfg((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), dlp_cfg);
    if (imu.status != ICM_20948_Stat_Ok) {
        Serial.print("Error setting digital-low-pass-filter: ");
        Serial.println(imu.statusString());
    }

    // Turn on the DLPLF (TODO error handling)
    imu.enableDLPF(ICM_20948_Internal_Acc, true);
    imu.enableDLPF(ICM_20948_Internal_Gyr, true);
*/

    // Startup magnometer
    imu.startupMagnetometer();
    if (imu.status != ICM_20948_Stat_Ok) {
        Serial.print("Error starting up magnometer: ");
        Serial.println(imu.statusString());
    }

    // Setup filter
    filter.begin(FUSION_UPDATES_PER_SECOND);
}


void loop() {
    // Is Some data ready from the IMU?
    if (imu.dataReady()) {
        imu.getAGMT();          // Retrieves data

        // Place into container variables
        ax = imu.accX();
        ay = imu.accY();
        az = imu.accZ();

        gx = imu.gyrX();
        gy = imu.gyrY();
        gz = imu.gyrZ();

//        // TODO might need to invert some of these axis
        mx = -imu.magX();
        my = -imu.magY();
        mz = -imu.magZ();
    }

    // If enough time has passed, update the fusion state
    const unsigned long current_us = micros();
    if (current_us >= (prev_fusion_update_us + FUSION_UPDATE_DELTA_US)) {
        // Mark the time we updated the fusion state
        prev_fusion_update_us = current_us;

        // Update the sensor fusion state
        filter.update(
            gx, gy, gz,     // These units might be in degrees and not radiasn
            ax, ay, az,
            mx, my, mz
        );

        // Read data
        heading = filter.getYaw();
        pitch = filter.getPitch();
        roll = filter.getRoll();
    }

    // If enough time has passed, send an update of the orientation over the serial connection
    const unsigned long current_millis = millis();
    if (current_millis >= (prev_serial_update_ms + SERIAL_UPDATE_DELTA_MS)) {
        // Mark the time we send the data over
        prev_serial_update_ms = current_millis;

        // Send Orientation data to the PC
        Serial.print("HPR ");                       // HPR = Heading, Pitch, Roll
        Serial.print(heading, SERIAL_PRECISION);
        Serial.print(" ");
        Serial.print(pitch, SERIAL_PRECISION);
        Serial.print(" ");
        Serial.print(roll, SERIAL_PRECISION);
        Serial.println("");     // End the line reading
    }
}

I’ll take a look at the calibration resources. Thanks!

Hi @def-pri-pub,

I’d also suggest taking a look at this GitHub issue: https://github.com/sparkfun/SparkFun_IC … /issues/40

Paul has been making some good progress with DMP on the ICM-20948. Would be great if you could test it.

Cheers,

Adam

Hi,

I’ve been a little slow on testing out the DMP code with an Arduino MEGA. I’ve been a little fixated on getting this thing working with the Uno. :smiley: I’m not quite sure if the axis of the breakout board and the MadgwickAHRS code are different, I had to swap the X & Y axis when updating the filter. If I didn’t swap them, when trying to rotate about the X axis (pitch), the on screen model would rotate about the Y axis (roll), and vice-versa

 // Update the sensor fusion state
 //   - The axis of the Accel/Gyro vs. the magnometer aren't the same, so they need to be flipped to match
 //   - I think that the MadgwickAHRS is using a different axis, so that's why X & Y are swapped
 filter.update(
      gy, gx,  gz,
      ay, ax,  az,
     -my, mx, -mz
 );

I’ve gotten it to work much better, but I’m still having issues with drift, understanding calibration, and the Euler angles output. I have a video recorded of where this is at right now:

https://www.youtube.com/watch?v=OrI-3uFmiHM

I’m really perplexed as to why when I rotate about the X axis (pitch) to around +90 deg (give or take a few) it will also roll about the Y axis too. Is this something about Euler angles for orientation I don’t understand? You can see this at 0:45 seconds into the video.

I’ve uploaded the entirety of my code thus far (Arduino & Python/Panda3d) to this repo if anyone else wants to give it a go or maybe point out what I’m missing.

https://github.com/define-private-publi … ualization

If you want to look specifically at my Arduino code it’s here: https://github.com/define-private-publi … rmware.ino

You won’t get anywhere until you completely understand how the axes of the individual sensors are oriented with respect to each other. Swap axes until they are self consistent and form a right handed coordinate system. Whatever axis you choose to be direction X for either the Madgwick or Mahony filters will correspond to yaw=0 when pointing at magnetic North.

Individually determine the directions of positive acceleration for the accelerometer by simply placing it on a table in various orientations as shown in this tutorial: https://learn.adafruit.com/adxl345-digi … rogramming. The measured acceleration due to gravity points UP (from the table pushing up, actually). In the northern hemisphere, the Earth’s magnetic field points DOWN and NORTH, into the ground.

so that’s why X & Y are swapped

That will never work, because the swap turns the right handed coordinate system into left handed, contrary to the assumption in the fusion code.

Okay, I looked at some of the raw output from the sensors when I placed the chip in certain orientations and this is what I was able to discover:

What do you conclude from those experiments?

This code demonstrates one several possible correct axis configurations, and works as expected: https://github.com/jremington/ICM_20948-AHRS