IMU SparkFun 9DOF ICM-20948: getting weird measures

I have a SparkFun 9DoF IMU Breakout - ICM-20948: SparkFun 9DoF IMU Breakout - ICM-20948 (Qwiic) - SEN-15335 - SparkFun Electronics

Lying on a table without a move and connected with Qwiic cable to a raspberrypi (using I2C with IMU being the 0x69 I2C slave): I should get Ax=0, Ay=0, Az=1, Wx=0, Wy=0, Wz=0.

After reading the doc https://cdn.sparkfun.com/assets/learn_tutorials/8/9/3/DS-000189-ICM-20948-v1.3.pdf, running this on the RPi gives

$ cat dummy.c 
#include <stdio.h>
#include <stdlib.h> // strtol

#ifdef BIG_ENDIAN
#define GET_DATA_FROM_RAW_BYTES(DATA_HIGH, DATA_LOW) (((DATA_HIGH) << 8) | (0x00FF & (DATA_LOW) ))
#else
#define GET_DATA_FROM_RAW_BYTES(DATA_HIGH, DATA_LOW) (((DATA_LOW)  << 8) | (0x00FF & (DATA_HIGH)))
#endif

int main(int argc, char ** argv) {
  printf("%s=%ld %s=%ld: %d\n", argv[1], strtol(argv[1], NULL, 16),
                                argv[2], strtol(argv[2], NULL, 16),
                                GET_DATA_FROM_RAW_BYTES(strtol(argv[1], NULL, 16), strtol(argv[2], NULL, 16)));
  return 0;
}

$ gcc -o dummy dummy.c

$ cat dummy.sh 
#!/bin/bash

# Setup: reset device + get out of sleep mode
i2cset -y 1 0x69 0x7f 0x00 # set bank 0: REG_BANK_SEL
i2cset -y 1 0x69 0x06 0x41 # on bank 0, reset: PWR_MGMT_1
i2cset -y 1 0x69 0x06 0x00 # on bank 0, not sleep: PWR_MGMT_1
i2cset -y 1 0x69 0x07 0x00 # on bank 0, enable accel/gyro: PWR_MGMT_2

# Config accel/gyro
i2cset -y 1 0x69 0x7f 0x20 # set bank 2: REG_BANK_SEL
i2cset -y 1 0x69 0x14 0x01 # set +-2g: ACCEL_CONFIG
i2cset -y 1 0x69 0x15 0x00 # set test OFF: ACCEL_CONFIG_2
i2cset -y 1 0x69 0x01 0x01 # set +-250dps: GYRO_CONFIG
i2cset -y 1 0x69 0x02 0x00 # set test OFF: GYRO_CONFIG_2

# Get accel/gyro measures
i2cset -y 1 0x69 0x7f 0x00 # set bank 0: REG_BANK_SEL
export ACCEL_XOUT_H="$(i2cget -y 1 0x69 0x2d)"
export ACCEL_XOUT_L="$(i2cget -y 1 0x69 0x2e)"
export ACCEL_YOUT_H="$(i2cget -y 1 0x69 0x2f)"
export ACCEL_YOUT_L="$(i2cget -y 1 0x69 0x30)"
export ACCEL_ZOUT_H="$(i2cget -y 1 0x69 0x31)"
export ACCEL_ZOUT_L="$(i2cget -y 1 0x69 0x32)"
echo "ACCEL_XOUT_H = $ACCEL_XOUT_H"
echo "ACCEL_XOUT_L = $ACCEL_XOUT_L"
echo "ACCEL_YOUT_H = $ACCEL_YOUT_H"
echo "ACCEL_YOUT_L = $ACCEL_YOUT_L"
echo "ACCEL_ZOUT_H = $ACCEL_ZOUT_H"
echo "ACCEL_ZOUT_L = $ACCEL_ZOUT_L"
echo "ACCEL_XOUT $(./dummy $ACCEL_XOUT_H $ACCEL_XOUT_L)"
echo "ACCEL_YOUT $(./dummy $ACCEL_YOUT_H $ACCEL_YOUT_L)"
echo "ACCEL_ZOUT $(./dummy $ACCEL_ZOUT_H $ACCEL_ZOUT_L)"
export GYRO_XOUT_H="$(i2cget -y 1 0x69 0x33)"
export GYRO_XOUT_L="$(i2cget -y 1 0x69 0x34)"
export GYRO_YOUT_H="$(i2cget -y 1 0x69 0x35)"
export GYRO_YOUT_L="$(i2cget -y 1 0x69 0x36)"
export GYRO_ZOUT_H="$(i2cget -y 1 0x69 0x37)"
export GYRO_ZOUT_L="$(i2cget -y 1 0x69 0x38)"
echo "GYRO_XOUT_H = $GYRO_XOUT_H"
echo "GYRO_XOUT_L = $GYRO_XOUT_L"
echo "GYRO_YOUT_H = $GYRO_YOUT_H"
echo "GYRO_YOUT_L = $GYRO_YOUT_L"
echo "GYRO_ZOUT_H = $GYRO_ZOUT_H"
echo "GYRO_ZOUT_L = $GYRO_ZOUT_L"
echo "GYRO_XOUT $(./dummy $GYRO_XOUT_H $GYRO_XOUT_L)"
echo "GYRO_YOUT $(./dummy $GYRO_YOUT_H $GYRO_YOUT_L)"
echo "GYRO_ZOUT $(./dummy $GYRO_ZOUT_H $GYRO_ZOUT_L)"



$ ./dummy.sh 
ACCEL_XOUT_H = 0x02
ACCEL_XOUT_L = 0xc4
ACCEL_YOUT_H = 0xfe
ACCEL_YOUT_L = 0x44
ACCEL_ZOUT_H = 0x3e
ACCEL_ZOUT_L = 0xc4
ACCEL_XOUT 0x02=2 0xc4=196: 708
ACCEL_YOUT 0xfe=254 0x44=68: 65092
ACCEL_ZOUT 0x3e=62 0xc4=196: 16068
GYRO_XOUT_H = 0xcd
GYRO_XOUT_L = 0x83
GYRO_YOUT_H = 0xef
GYRO_YOUT_L = 0x96
GYRO_ZOUT_H = 0xf2
GYRO_ZOUT_L = 0x81
GYRO_XOUT 0xcd=205 0x83=131: 52611
GYRO_YOUT 0xef=239 0x96=150: 61334
GYRO_ZOUT 0xf2=242 0x81=129: 62081

The only value whihc is close to expected is Az (16068 is close to 16384 = Accel_sensitivity <=> 16384 = 1g). All other values are unexpected and not even to what they should be?!..

What’s wrong?

The rate gyro needs to be calibrated, at least to remove the offsets. Most people just add up a few hundred readings while the sensor is stationary, and subtract the average values (one for each axis) from subsequent readings.

Pay attention to signed versus unsigned integers! This is a small negative value:

ACCEL_YOUT 0xfe=254 0x44=68: 65092

The magnetometer also needs to be calibrated. This tutorial gives the best overview and methods: Tutorial: How to calibrate a compass (and accelerometer) with Arduino | Underwater Arduino Data Loggers

Accuracy will be improved if the accelerometer is also calibrated, as suggested above.

Thanks for this useful hint. You mean that I have to substract XA_OFFS (built from XA_OFFS_H and XA_OFFS_L) from ACCEL_XOUT, and, to substract XG_OFFS_USR (built from XG_OFFS_USRH and XG_OFFS_USRH) from GYRO_XOUT? Did I get your remark right?

Not sure how to remove the offset neither if XA_OFFS_* and XG_OFFS_USR* should be used and how… Can you detail on this?

Most people just add up a few hundred readings while the sensor is stationary, and subtract the average values

Or did you mean, I am supposed to average values coming out from the accelerometers / gyroscopes, average it, and substarct the mean from upcoming measurements ?..

Pay attention to signed versus unsigned integers

Also here not sure to fully understand. Can you precise what’s wring in the code? how to get the correct value?

Only the rate gyro is calibrated as I described. Follow the link to learn how magnetometers and accelerometers are calibrated.

The raw data type from the sensor is signed integer. Your code is treating the data as unsigned integers.

Now I have this

$ cat dummy.c
#include <stdio.h>
#include <stdlib.h> // strtol
#include <stdint.h> // int16_t, int8_t
#include <inttypes.h> // PRIx16

#ifdef BIG_ENDIAN
#define GET_DATA_FROM_BYTES(DATA_HIGH, DATA_LOW) (int16_t) (((DATA_HIGH) << 8) | (0x00FF & (DATA_LOW) ))
#else
#define GET_DATA_FROM_BYTES(DATA_HIGH, DATA_LOW) (int16_t) (((DATA_LOW)  << 8) | (0x00FF & (DATA_HIGH)))
#endif

int main(int argc, char ** argv) {
  int8_t data_high = (int8_t) strtol(argv[1], NULL, 16);
  int8_t data_low  = (int8_t) strtol(argv[2], NULL, 16);
  int16_t data = GET_DATA_FROM_BYTES(data_high, data_low);
  printf("%s=%d %s=%d: 0x%" PRIx16 " %d %f\n", argv[1], data_high,
                                               argv[2], data_low,
                                               (uint16_t) data, data, (float) (data));
  return 0;
}
$ 
$ gcc -o dummy dummy.c
$ 
$ cat dummy.sh
#!/bin/bash

# Setup: reset device + get out of sleep mode
i2cset -y 1 0x69 0x7f 0x00 # set bank 0: REG_BANK_SEL
i2cset -y 1 0x69 0x06 0x41 # on bank 0, reset: PWR_MGMT_1
sleep 1 # let the HW reset
i2cset -y 1 0x69 0x06 0x00 # on bank 0, not sleep: PWR_MGMT_1
i2cset -y 1 0x69 0x07 0x00 # on bank 0, enable accel/gyro: PWR_MGMT_2

# Config accel/gyro
i2cset -y 1 0x69 0x7f 0x20 # set bank 2: REG_BANK_SEL
i2cset -y 1 0x69 0x14 0x01 # set +-2g: ACCEL_CONFIG
i2cset -y 1 0x69 0x15 0x00 # set test OFF: ACCEL_CONFIG_2
i2cset -y 1 0x69 0x01 0x01 # set +-250dps: GYRO_CONFIG
i2cset -y 1 0x69 0x02 0x00 # set test OFF: GYRO_CONFIG_2

# Get accel/gyro measures
i2cset -y 1 0x69 0x7f 0x00 # set bank 0: REG_BANK_SEL
export ACCEL_XOUT_H="$(i2cget -y 1 0x69 0x2d)"
export ACCEL_XOUT_L="$(i2cget -y 1 0x69 0x2e)"
export ACCEL_YOUT_H="$(i2cget -y 1 0x69 0x2f)"
export ACCEL_YOUT_L="$(i2cget -y 1 0x69 0x30)"
export ACCEL_ZOUT_H="$(i2cget -y 1 0x69 0x31)"
export ACCEL_ZOUT_L="$(i2cget -y 1 0x69 0x32)"
echo "ACCEL_XOUT_H = $ACCEL_XOUT_H"
echo "ACCEL_XOUT_L = $ACCEL_XOUT_L"
echo "ACCEL_YOUT_H = $ACCEL_YOUT_H"
echo "ACCEL_YOUT_L = $ACCEL_YOUT_L"
echo "ACCEL_ZOUT_H = $ACCEL_ZOUT_H"
echo "ACCEL_ZOUT_L = $ACCEL_ZOUT_L"
echo "ACCEL_XOUT $(./dummy $ACCEL_XOUT_H $ACCEL_XOUT_L)"
echo "ACCEL_YOUT $(./dummy $ACCEL_YOUT_H $ACCEL_YOUT_L)"
echo "ACCEL_ZOUT $(./dummy $ACCEL_ZOUT_H $ACCEL_ZOUT_L)"
export GYRO_XOUT_H="$(i2cget -y 1 0x69 0x33)"
export GYRO_XOUT_L="$(i2cget -y 1 0x69 0x34)"
export GYRO_YOUT_H="$(i2cget -y 1 0x69 0x35)"
export GYRO_YOUT_L="$(i2cget -y 1 0x69 0x36)"
export GYRO_ZOUT_H="$(i2cget -y 1 0x69 0x37)"
export GYRO_ZOUT_L="$(i2cget -y 1 0x69 0x38)"
echo "GYRO_XOUT_H = $GYRO_XOUT_H"
echo "GYRO_XOUT_L = $GYRO_XOUT_L"
echo "GYRO_YOUT_H = $GYRO_YOUT_H"
echo "GYRO_YOUT_L = $GYRO_YOUT_L"
echo "GYRO_ZOUT_H = $GYRO_ZOUT_H"
echo "GYRO_ZOUT_L = $GYRO_ZOUT_L"
echo "GYRO_XOUT $(./dummy $GYRO_XOUT_H $GYRO_XOUT_L)"
echo "GYRO_YOUT $(./dummy $GYRO_YOUT_H $GYRO_YOUT_L)"
echo "GYRO_ZOUT $(./dummy $GYRO_ZOUT_H $GYRO_ZOUT_L)"
$ 
$ ./dummy.sh 
ACCEL_XOUT_H = 0x01
ACCEL_XOUT_L = 0x38
ACCEL_YOUT_H = 0xfe
ACCEL_YOUT_L = 0x94
ACCEL_ZOUT_H = 0x3e
ACCEL_ZOUT_L = 0xd0
ACCEL_XOUT 0x01=1 0x38=56: 0x138 312 312.000000
ACCEL_YOUT 0xfe=-2 0x94=-108: 0xfe94 -364 -364.000000
ACCEL_ZOUT 0x3e=62 0xd0=-48: 0x3ed0 16080 16080.000000
GYRO_XOUT_H = 0x1e
GYRO_XOUT_L = 0xef
GYRO_YOUT_H = 0x00
GYRO_YOUT_L = 0xa3
GYRO_ZOUT_H = 0x1d
GYRO_ZOUT_L = 0xc2
GYRO_XOUT 0x1e=30 0xef=-17: 0x1eef 7919 7919.000000
GYRO_YOUT 0x00=0 0xa3=-93: 0xa3 163 163.000000
GYRO_ZOUT 0x1d=29 0xc2=-62: 0x1dc2 7618 7618.000000
$

I get correct values for the accelerometers (even if I still miss the calibration - Following the link Tutorial: How to calibrate a compass (and accelerometer) with Arduino | Underwater Arduino Data Loggers I still need to use Magneto or some other software to make the ellipsoid an circle - didn’t had time to go through this but I understand the steps to follow to get calibrated accelerometers values)

But, I have still crazy values for gyroscopes?!.. The scale is +/- 250 dps and the IMU is at rest on the table: should get wx=wy=wz=0!

The raw data type from the sensor is signed integer. Your code is treating the data as unsigned integers.

This should be fixed now.

Can you detail this? Should I:

  • “just” add up a few hundred readings while the sensor is stationary, and subtract the average values (one for each axis) from subsequent readings?
  • use magneto to calibrate the gyro too?
  • add or substract XG_OFFS_USR (built from XG_OFFS_USRH and XG_OFFS_USRH) from GYRO_XOUT? (feel like it’s related to calibration - but no idea how/when/why to use this)

Examples for everything can be found in the Arduino code I posted for that sensor:

1 Like

This tutorial gives the best overview and methods: Tutorial: How to calibrate a compass (and accelerometer) with Arduino | Underwater Arduino Data Loggers

After a second read, my understanding is that the “Least Squares Ellipsoid Specific Fitting” algorithm from Li, use in Magneto, can calibrate magnetometer and accelerometer but not gyroscopes: is this correct?

Correct. Only offsets are needed for the rate gyro.

OK thanks for this clarification.

AFAIU, on one side, the Li algorithm corrects non-orthogonality (of the 3 sensible axis), scale (magnitude) and bias (offset), and, on the other side, all sensors (magnetometer, accelerometer and gyroscopes) suffer from these problems… So I didn’t really get why gyroscopes seemed to be “only” offset corrected: if you know why, I’d be glad to know.

In 9DOF AHRS fusion algorithms, the accelerometer and magnetometer define the transformation between the body and fixed frames of reference, by defining Up and North.

The rate gyroscope is primarily used to temporarily correct the errors introduced by the assumption that the accelerometer defines “Up” when the sensor is rotating. Consequently, the rate gyro has no influence if the sensor is not rotating.

In practice, the rate gyro scale factors are orders of magnitude less critical than the offsets and so the errors in the scale factors can usually be ignored.

2 Likes