ICM-90248 Gives wrong Accelerometer values when using the low-pass filter

I’m using the ICM-90248 breakout module via I2C with the Sparkfun Thing esp32 board. When I set the low-pass filter cutoff frequency to anything other than “acc_d473bw_n499bw”, the output values from the accelerometer are erroneously decreased by a large factor (8 to 256 depending on which cutoff frequency setting I use). When I use “acc_d473bw_n499bw”, all is well as the accelerometer z axis output is approximately 16384 bits with the full-scale range set to +/- 2 g’s - as expected.

Am I doing something wrong in the setup (see below)? Is there any work-around for this? Any idea why it is happening?

So, for example, if I use “acc_d246bw_n265bw” as the LPF cutoff frequency setting, the output of the accelerometer z-axis is about 2048, a factor of 8 less than expected. If I flip the board upside down with this same setting, the acc.z output is about -2048. So, the problem isn’t an offset, it is a reduction in gain by a factor of 8. I also considered that maybe somehow the full-scale is getting set to +/- 16 g’s whereas I’m expecting it to be +/- 2 g’s but that is not the case. If I change the fsr to +/- 16 g’s using “acc_d246bw_n265bw”, the acc.z output decreases by another factor of 8 to give about 256 (whereas 2048 is expected).

The acc.z value gets cut by another factor of 2 each time the lpf cutoff frequency is cut in half. Here are the approximate acc.z values with the board lying flat on a table for each available lpf cutoff frequency setting with the fsr set to +/- 2 g’s. Each value should be about 16384.

acc_d246bw_n265bw : 2048
acc_d246bw_n265bw_1 : 2048
acc_d111bw4_n136bw : 1024
acc_d50bw4_n68bw8 : 512
acc_d23bw9_n34bw4 : 256
acc_d11bw5_n17bw : 128
acc_d5bw7_n8bw3 : 64
acc_d473bw_n499bw : 16384 (the expected value for each setting)

All of the values above are decreased by a factor of 2 when using an fsr of +/- 4 g’s, a factor of 4 when using +/- 8 g’s and a factor of 8 when using +/- 16 g’s. So, with “acc_d5bw7_n8bw3” and +/- 16 g’s, the accelerometer z-axis output with the sensor lying flat on a table is about 8 whereas it is expected to be 16384. I’ve tried all combinations of cutoff frequency and fsr and all combinations exhibit the erroneous behavior described except when the cutoff frequency is set to “acc_d473bw_n499bw”.

All of the examples cited above refer only to the z-axis but the same errors occur in the x and y axes too. I don’t have a way of determining whether the same applies to the gyro.

There is a comment in the Sparkfun ICM_90248 library file ICM_20948_C.c file at line 1091 that hints of the behavior described above but there is no indication of the cause or remedy. Here’s a snippet from ICM_20948_C.c:

 pagmt->fss.a = acfg.ACCEL_FS_SEL; // Worth noting that without explicitly setting the FS range of the accelerometer it was showing the register value for +/- 2g but the reported values were actually scaled to the +/- 16g range
                                    // ***Wait a minute... now it seems like this problem actually comes from the digital low-pass filter. When enabled the value is 1/8 what it should be...***

Here’s the setup code I’m using for the ICM-20948. The sample rate divisor is set to 22 to get a sample frequency of about 48.9 hz.

  // Initialize IMU Communication
  bool initialized = false;
  while (!initialized)
  {
    // Start the IMU SPI or I2C unit
    #ifdef USE_SPI
        myICM.begin(CS_PIN, SPI_PORT, SPI_FREQ); // Here we are using the user-defined SPI_FREQ as the clock speed of the SPI bus
    #else
        myICM.begin(WIRE_PORT, AD0_VAL);
    #endif

    // Report Status
    SERIAL_PORT.print(F("Initialization of the sensor returned: "));
    SERIAL_PORT.println(myICM.statusString());
    if (myICM.status != ICM_20948_Stat_Ok)
    {
      SERIAL_PORT.println("Trying again...");
      delay(500);
    }
    else
    {
      initialized = true;
    }
  }
  SERIAL_PORT.println("Device connected!");

  // Reset the IMU so that it starts from a known state
  myICM.swReset();
  if (myICM.status = ICM_20948_Stat_Ok)
  {
    SERIAL_PORT.print(F("Software Reset returned: "));
    SERIAL_PORT.println(myICM.statusString());
  }
  delay(250);

  // Wake the IMU
  myICM.sleep(sensorSleep);
  myICM.lowPower(false);

  // The next few configuration functions accept a bit-mask of sensors for which the settings should be applied.

  // Configure the Gyro, Accelerometer & Magnetometer in the IMU module & set the IMU sample mode and rate

  // Set the sample MODE for the Gyro and Accelerometer
  // options: ICM_20948_Sample_Mode_Continuous - Produces new data as fast as possible
  //          ICM_20948_Sample_Mode_Cycled - Produces new data periodically
  myICM.setSampleMode((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), ICM_20948_Sample_Mode_Cycled); // Gyro, Accel & Mag are all sampled at the Gyro rate
  SERIAL_PORT.print(F("setSampleMode returned: "));
  SERIAL_PORT.println(myICM.statusString());
  
  // Set the Gyro Sample RATE - All other data will be sampled at the same rate
  ICM_20948_smplrt_t mySmplrt;
  mySmplrt.g = SAMPLE_RATE_DIVISOR; // 1125/fs-1
  myICM.setSampleRate(ICM_20948_Internal_Gyr, mySmplrt);
  SERIAL_PORT.print(F("setSampleRate returned: "));
  SERIAL_PORT.println(myICM.statusString());
  
  // Set full-scale ranges for both acc and gyr
  ICM_20948_fss_t myFSS; // This uses a "Full Scale Settings" structure that can contain values for all configurable sensors
  myFSS.a = gpm2; // (ICM_20948_ACCEL_CONFIG_FS_SEL_e), Choices are: gpm2, gpm4, gpm8, gpm16.  "gpm" = G's, +/-
  myFSS.g = dps250; // (ICM_20948_GYRO_CONFIG_1_FS_SEL_e), Choices are: dps250, dps500, dps1000, dps2000.  "dps" = Degrees/sec

  delay(100);

  myICM.setFullScale((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), myFSS);
    SERIAL_PORT.print(F("setFullScale returned: "));
    SERIAL_PORT.println(myICM.statusString());

//  }
 delay(100);
 
  // Choose whether or not to use DLPF
  // Here we're also showing another way to access the status values, and that it is OK to supply individual sensor masks to these functions
  ICM_20948_Status_e accDLPEnableStat = myICM.enableDLPF(ICM_20948_Internal_Acc, true);
  ICM_20948_Status_e gyrDLPEnableStat = myICM.enableDLPF(ICM_20948_Internal_Gyr, true);
  SERIAL_PORT.print(F("Enable DLPF for Accelerometer returned: "));
  SERIAL_PORT.println(myICM.statusString(accDLPEnableStat));
  SERIAL_PORT.print(F("Enable DLPF for Gyroscope returned: "));
  SERIAL_PORT.println(myICM.statusString(gyrDLPEnableStat));

 /* #################  NOTE  #################
  * Changing the filter cutoff frequency may change the required calibration constants.
  * ##########################################
  */

  // Setup IMU Digital Low-Pass Filter configuration
  // The Nyquist Bandwidth is simply half of the sample rate used for the filter in the DLP for each selection.
  // The filters used are simple moving averages of different numbers of samples for each setting.

  ICM_20948_dlpcfg_t myDLPcfg;    // Similar to FSS, this uses a configuration structure for the desired sensors
  myDLPcfg.a = acc_d5bw7_n8bw3; // (ICM_20948_ACCEL_CONFIG_DLPCFG_e)
 
  myDLPcfg.g = gyr_d361bw4_n376bw5; // (ICM_20948_GYRO_CONFIG_1_DLPCFG_e)
 
  myICM.setDLPFcfg((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), myDLPcfg);
  if (myICM.status = ICM_20948_Stat_Ok)
  {
    SERIAL_PORT.print(F("setDLPcfg returned: "));
    SERIAL_PORT.println(myICM.statusString());
  }
  
  // Start the Magnetometer
  myICM.startupMagnetometer();
//  if (myICM.status = ICM_20948_Stat_Ok)
//  {
    SERIAL_PORT.print(F("startupMagnetometer returned: "));
    SERIAL_PORT.println(myICM.statusString());
//  }

  // Setup IMU interrupts. Many options, but for this just configuring the interrupt pin to signal that new data is ready
    myICM.cfgIntActiveLow(true);  // Active low to be compatible with the breakout board's pullup resistor
    myICM.cfgIntOpenDrain(false); // Push-pull, though open-drain would also work thanks to the pull-up resistors on the breakout
    myICM.cfgIntLatch(false);      // false = Interrupt signal is cleared by IMU immediately after being set, true = interrupt latched until manually cleared (by isr)
    SERIAL_PORT.print(F("cfgIntLatch returned: "));
    SERIAL_PORT.println(myICM.statusString());

    SERIAL_PORT.print("CPU Clock Frequency (MHz) = ");
    SERIAL_PORT.println(getCpuFrequencyMhz(),1);
    SERIAL_PORT.println("Configuration Complete, enabling IMU Interrupts now!");

Hi Rodney,

The only thing that jumps out at me is that you are using ICM_20948_Sample_Mode_Cycled. Do you see the same thing with Continuous?

Looking at Example2, are you using the equivalent of printRawAGMT or printScaledAGMT to print the values?

I hope this helps,
Paul

The ICM-20948 seems to scale the output data differently depending on the DLPF settings. When you select a lower bandwidth (i.e., lower cutoff frequency), the device might be applying a different internal scaling factor, which could lead to the observed reduction in output values.

The ICM-20948 data sheet doesn’t seem to explain how the low pass filter works, or is supposed to work. Check the manufacturer’s site for an application note on the topic.

An option would be to implement your own, as some sort of moving average or exponential filter. That way you know exactly what you are getting.

TDK Invensense has a long history of being secretive about their internal algorithms (such as the DMP), perhaps because they have not worked very well.

Thanks so much, Paul! Changing to continuous mode completely solved the problem. Now, all filter cutoff settings produce the correct output at 1 g in the z-axis for all FSR values. I’m using a print method essentially the same as printRawAGMT.

Not sure how I would have ever gleaned that from the datasheet but I’ll study it a bit more.

Thanks again,
Rodney.

As Paul correctly surmised, the DLPF only works correctly in the continuous sample mode. My best guess is that the filter isn’t getting the right data stream in sampled mode and that influences gain of the filter. Whatever it is, problem solved!

I agree, the datasheet is lacking when it comes to the DLPF. I’ll look again for an app note but I’m not holding my breath. Paul got me all straighted out on this issue so I’m moving on to the next piece of the puzzle.

I’ve implemented my own filter already and I intend to use it in addition to the DLPF on the device because I want a 4th order filter at a specific cutoff frequency. However, the goal of using the DLPF is to avoid aliasing and as there are 9 channels that would put an unnecessary burden on my esp32 to have a sample rate high enough to avoid aliasing with 9 filters running every sample. My understanding is that the device samples internally in the 4 khz range so the onboard filter would likely eliminate aliasing without the MCU burden.

Rodney.