How to use the I2C_MST of the ICM20948 to read aux. sensor data

Hello everyone,

I'm currently working on a driver for the relatively new ICM20948 by InvenSense for Arduino. ( Accelerometer, Gyroscope and Magnetometer on a single chip)

icm20948 datasheet

I communicate via SPI and had no problems with neither the Accel nor the Gyroscope.

The Magnetometer (AK09916) on the other hand does not work with SPI directly.
To read and write from/to the Magnetometer Registers we have to use the I2C_Slave and the I2C_Master Registers.

Since the Datasheet doesn't provide a clear answer of how exactly to do that, (Or at least I can't figure it out) I'm hoping someone here can help me.
Even if it is not the same chip, it is very similar to the mpu9250 in a lot of ways. So if any of you figured out how to use the slave registers to acces the mpu9250 aux. magnetometer, it might be just what i'm looking for.

I still haven't managed to receive any data from the Magnetometer, so the first goal is simply reading the WHO_AM_I Register.

My Init function:

uint8_t ICM20948::init() {

  uint8_t okByte = 0;

  set_SPI(icm20948_ss_pin, icm20948_spi_settings, ICM20948_spi_read_mask);
  delay(100);

  if(get_WHO_AM_I_Reg() == 0xEA) {

    reset();

    set_CLKSEL_Bits(get_CLKSEL_value());
    set_I2C_IF_DIS_Bit(true);

    if(is_GYRO_SETTING_VALID()) setup_gyro();
    else okByte |= 0b00000010;

    if(is_ACCEL_SETTING_VALID()) setup_accel();
    else okByte |= 0b00000100;

    if(is_TEMP_SETTING_VALID()) setup_temp();
    else okByte |= 0b00001000;

    if(is_FIFO_SETTING_VALID()) setup_Fifo();
    else okByte |= 0b00010000;

    setup_i2c_slave0(ICM20948_MAG_ADDRESS);            // THE PROBLEM

    Serial.println(read_Mag_Register(0x01));                         // <= Trying to read WHO_AM_I

  }
  else {
    okByte |= 0b00000001;
  }
  return okByte;
}

Setting up the I2C_Mst and slave0 Registers

void ICM20948::setup_i2c_slave0(uint8_t slave_addr) {

  select_user_bank(0);

  // Settings in Usr Ctrl Register
  set_I2C_MST_EN_Bit(true);

  // Settings in INT_PIN_CFG Register
  set_INT1_OPEN_Bit(false);
  set_INT1_LATCH_EN_Bit(true);
  set_INT_ANYRD_2CLEAR_Bit(false);
  set_BYPASS_EN_Bit(true);

  // Settings in INT_ENABLE Register
  set_I2C_MST_INT_EN_Bit(true);

  // Changes in INT_ENABLE_1 Register
  set_RAW_DATA_0_RDY_EN_Bit(true);

  // Changes in LP_CONFIG Register
  set_I2C_MST_CYCLE_Bit(false);

  select_user_bank(2);

  set_ODR_ALIGN_EN_Bit(false);

  select_user_bank(3);

  // Setup I2C_MST_ODR_CONFIG Register
  set_I2C_MST_ODR_CONFIG_Bits(0x00);      // Shouldn't matter since gyro is enabled

  // Setup I2C_MST_CTRL Register          // Datasheet page 68
  set_MULT_MST_EN_Bit(false);             // Only for Multiple SLV Devices?
  set_I2C_MST_P_NSR_Bit(true);            // Not sure ?! Restart/Stop after read
  set_I2C_MST_CLK_Bits(0x7);              // recommended I2C_MST_CLK (Datasheet page 81);

  // Setup I2C_MST_DELAY_CTRL Register    // Datasheet page 69
  set_DELAY_ES_SHADOW_Bit(false);         // Not sure ?!
  set_I2C_SLV0_DELAY_EN_Bit(false);       // ODR Div. Register = 0 anyway.Sort of Smplrt_Div on/off (set at I2C_MST_ODR_CONFIG)

  // Setup I2C_SLV0_ADDR Register         // Datasheet page 69
  set_I2C_ID_0_Bits(slave_addr);          // Bits [6:0] of I2C_SLV0_ADDR set to Mag_Addr (0x0C)

  // Setup I2C_SLV0_CTRL Register         // Datasheet page 70
  set_I2C_SLV0_BYTE_SW_Bit(false);        // swap receiving Bytes (Check if needed once any data is received)
  set_I2C_SLV0_GRP_Bit(false);            // f.e. 2 Byte Group always ends with odd/even Reg. Addr (Check if needed once any data is received)
  set_I2C_SLV0_REG_DIS_Bit(false);        // Not sure ?!
  set_I2C_SLV0_EN_Bit(true);              // Enable reading data from slv 0. Stored at EXT_SENS_DATA_00

  select_user_bank(0);
}

Read/Write from/to Magnetometer

uint8_t ICM20948::read_Mag_Register(uint8_t _mag_register) {
  select_user_bank(3);
  set_I2C_SLV0_RNW_Bit(true);             // Set Bit [7] of I2C_SLV0_ADDR to 1 => Transfer is a read
  set_I2C_SLV0_REG_Reg(_mag_register);    // set i2c slv 0 register addr. from where to begin data transfer (0x01 => mag whoAmI)
  set_I2C_SLV0_LENG_Bits(0x01);           // Number of Bytes to be read from I2C slave 0
  select_user_bank(0);

  return read_Byte(ICM20948_EXT_SLV_SENS_DATA_00);
}

void ICM20948::write_Mag_Register(uint8_t _mag_register, uint8_t _value) {
  select_user_bank(3);
  set_I2C_SLV0_RNW_Bit(false);            // Set Bit [7] of I2C_SLV0_ADDR to 0 => Transfer is a write
  set_I2C_SLV0_REG_Reg(_mag_register);    // set i2c slv 0 register addr. from where to begin data transfer (0x01 => mag whoAmI)
  set_I2C_SLV0_LENG_Bits(0x01);           // Number of Bytes to be read from I2C slave 0
  set_I2C_SLV0_DO_Reg(_value);            // Value beeing stored at mag_register
  select_user_bank(0);
}

I've build a small function printing the registers for debug purposes which gives me the following console log after init:

|------------------------+---------------------|
|         ICM20948 - Register Overview         |
|------------------------+---------------------|
|||||||||||||||| [USER_BANK_0] |||||||||||||||||
|------------------------+---------------------|
|   WHO_AM_I             |   11101010 (0xEA)   |
|   USER_CTRL            |   01110000 (0x70)   |
|   LP_CONFIG            |   00000000 (0x00)   |
|   PWR_MGMT_1           |   00001001 (0x09)   |
|   PWR_MGMT_2           |   00000000 (0x00)   |
|   INT_PIN_CFG          |   00100010 (0x22)   |
|   INT_ENABLE           |   00000001 (0x01)   |
|   INT_ENABLE_1         |   00000001 (0x01)   |
|   INT_ENABLE_2         |   00000000 (0x00)   |
|   INT_ENABLE_3         |   00000000 (0x00)   |

|   FIFO_EN_1            |   00000000 (0x00)   |
|   FIFO_EN_2            |   00011110 (0x1E)   |
|------------------------+---------------------|
|||||||||||||||| [USER_BANK_2] |||||||||||||||||
|------------------------+---------------------|
|   GYRO_CONFIG_1        |   00110011 (0x33)   |
|   GYRO_CONFIG_2        |   00000000 (0x00)   |
|   GYRO_SMPLRT_DIV      |   00000000 (0x00)   |
|   ACCEL_CONFIG         |   00001001 (0x09)   |
|   ACCEL_CONFIG_2       |   00000000 (0x00)   |
|   ACCEL_SMPLRT_DIV     |   00001010 (0x0A)   |
|   TEMP_CONFIG          |   00000000 (0x00)   |
|   ODR_ALIGN_EN         |   00000000 (0x00)   |
|------------------------+---------------------|
|||||||||||||||| [USER_BANK_3] |||||||||||||||||
|------------------------+---------------------|
|   I2C_MST_ODR_CONFIG   |   00000000 (0x00)   |
|   I2C_MST_CTRL         |   00010111 (0x17)   |
|   I2C_MST_DELAY_CTRL   |   00000000 (0x00)   |
|   I2C_SLV0_ADDR        |   10001100 (0x8C)   |
|   I2C_SLV0_REG         |   00000001 (0x01)   |
|   I2C_SLV0_CTRL        |   10000001 (0x81)   |
|   I2C_SLV0_DO          |   00000000 (0x00)   |
|------------------------+---------------------|
OK

The more I tested and failed the less I felt I know what i'm doing :frowning:
Am i just missing one Bit? Does the different ODR mess with the communications? Do I somehow forget to trigger the transmit? You see, I'm kind of lost right now.

If you need some other information or have questions about my setup, plz let me know!

The DMP firmware uses the magnetometer or else the I2C bus is set in pass-through mode to make it available on the normal I2C bus.

Do you want to use the SPI registers to get to the data of the magnetometer ?
It that possible ? Because in the past it was a magnetometer made by others that was included. It was a normal I2C magnetometer. I suppose it still is.

In the past, with the MPU-9050, there was hacked DMP firmware, so the Arduino could use the DMP with the FIFO and interrupts.
Do you have the development environment of this new chip ? I suppose it comes with a number of pre-made DMP firmwares.

Where do all those functions come from ? For example "set_I2C_MST_EN_Bit()" and "set_I2C_SLV0_REG_Reg()".

Which Arduino board do you use ?

Thanks for your reply.
I'm still developing/testing the library and haven't started with the DMP function at all. From what I can tell, it shouldn't be required in order to read/write data from/to the Magnetometer.

It is infact still a normal I2C Magnetometer. From what I understand the I2C_Master and the slave Registers allow to communicate with the Magnetometer anyway.

There are a couple of Registers to achieve that goal:
I2C_MST_CTRL, (incl. I2C_Mst_clk etc.)
I2C_SLV0_ADDR, (storing the i2c address of device)
I2C_SLV0_REG, (stroring the device address of which we want to read/write to)
I2C_SLV0_CTRL (incl. enable slave, how many Bytes)
I2C_SLV0_DO (Data Out, data to be written on slave register)
(+ other Regs and other Bits involved in usr_ctrl and others)

The results of the reading should be in EXT_SLV_SENS_DATA_00

class ICM20948 : public ICM20948_interface , public ICM20948_config {

...

class ICM20948_interface : public SPI_helper {

...

the ICM20948_interface include all the functions you mentioned.

the ICM20948_config only the values I want to write to the registers at startup and some getter/setter methods.

  • at the moment I use it with a teensy 3.6 (180Mhz)

Reading the magnetometer without DMP firmware is indeed the hard part.
I don't understand this example for the MPU-9250, so I can't help any further, sorry :frowning: Maybe you can use it.

Are the Invensense drivers/examples online ?

There is a Application Note: "Migrating from MPU-9250 to ICM-20948".
I suppose they have upgraded and also redesigned it, keeping the interface similar to the MPU-9250.

Thx for this example.

I really liked his approach of initializing via matrix (and will probably copy that someday :slight_smile: ) but next to that I sadly still don't see what I'm missing. I have tried it with the register values the example uses. (Even the Bitorder/Bitname of the most obviously involved Registers are left unchanged with the ICM20948)

I also don't see any other registers involved in this example, that I haven't set in a similar way.

*push and thx again so far!

** you have to first register and then ask for permission to be able to download the driver from the official website. In the process of doing so I decided to try my luck with Technical support aswell.
Lets see how that turns out :slight_smile:

Fully functional driver with DMP & Mag support for both MPU-9250 and ICM-20948. It's generic C++ code portable to any platform.

Low level sensor access : EHAL/src/sensors at master · I-SYST/EHAL · GitHub
High level algo DMP : EHAL/src/imu at master · I-SYST/EHAL · GitHub

Open source firmware Hardware that can stream ICM-20948 data over Bluetooth is available here, the sensor board perk. BLYST Nano | Crowd Supply

I just want to understand, does this mean you can only have to use i2c to get magnetometer data or is this a workaround?