Skip to main content

Overview

The ICM-20948 is a 9-axis Motion Processing Unit (MPU) combining:
  • 3-axis gyroscope (angular velocity)
  • 3-axis accelerometer (linear acceleration)
  • 3-axis magnetometer (magnetic field / compass)
Adding an IMU to your robot provides orientation data, complementing wheel encoders which are prone to slippage.

Why IMU for Mecanum Robots?

Slip Compensation

Mecanum wheels slip frequently, especially during rotation. IMU provides ground-truth orientation independent of wheel odometry.

Sensor Fusion

Combine encoder odometry with IMU data using Extended Kalman Filter (EKF) for robust pose estimation.

Tilt Detection

Detect if robot is on a slope or uneven surface, enabling compensation or safety stops.

Dynamic Response

Measure actual angular velocity vs commanded, useful for tuning rotation controllers.

ICM-20948 Specifications

Sensor Ranges

SensorConfigurable RangesResolution
Gyroscope±250, ±500, ±1000, ±2000 °/s16-bit
Accelerometer±2, ±4, ±8, ±16 g16-bit
Magnetometer±4900 µT16-bit

Communication

  • Interface: I2C or SPI (we’ll use I2C)
  • I2C Address: 0x68 (default) or 0x69 (if AD0 pin HIGH)
  • Operating Voltage: 1.71V - 3.6V (3.3V typical)
  • Current: ~3.5mA active
ICM-20948 IMU breakout board

Wiring Diagram

I2C Connection to ESP32

ESP32                  ICM-20948
GPIO 21 (SDA) ────────→ SDA
GPIO 22 (SCL) ────────→ SCL
3.3V          ────────→ VCC
GND           ────────→ GND
Voltage: ICM-20948 is 3.3V! Do NOT connect to 5V or it will be damaged.

Pull-up Resistors

I2C requires pull-up resistors (typically 4.7kΩ) on SDA and SCL:
  • Most breakout boards have them built-in
  • ESP32 has internal pull-ups (can enable in software)
  • External pull-ups recommended for longer wires or multiple devices

Software Setup

Library Options

Complete Implementation

Basic IMU Reading

#include <Arduino.h>
#include <Wire.h>
#include "ICM_20948.h"

#define SDA_PIN 21
#define SCL_PIN 22

ICM_20948_I2C myICM;

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  
  // Initialize IMU
  bool initialized = false;
  while (!initialized) {
    myICM.begin(Wire, 1);  // AD0_VAL = 1 for address 0x69, 0 for 0x68
    
    if (myICM.status != ICM_20948_Stat_Ok) {
      Serial.println("IMU not connected. Retrying...");
      delay(500);
    } else {
      initialized = true;
      Serial.println("IMU connected!");
    }
  }
  
  // Configure sensor ranges
  ICM_20948_fss_t myFSS;
  myFSS.a = dfs2G;      // Accelerometer: ±2g
  myFSS.g = dfs250dps;  // Gyroscope: ±250 °/s
  myICM.setFullScale(ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr, myFSS);
  
  // Enable sensors
  myICM.enableDLPF(ICM_20948_Internal_Acc, true);
  myICM.enableDLPF(ICM_20948_Internal_Gyr, true);
}

void loop() {
  if (myICM.dataReady()) {
    myICM.getAGMT();  // Get all sensor data
    
    // Accelerometer (m/s²)
    float ax = myICM.accX();
    float ay = myICM.accY();
    float az = myICM.accZ();
    
    // Gyroscope (°/s)
    float gx = myICM.gyrX();
    float gy = myICM.gyrY();
    float gz = myICM.gyrZ();
    
    // Magnetometer (µT)
    float mx = myICM.magX();
    float my = myICM.magY();
    float mz = myICM.magZ();
    
    // Print data
    Serial.print("Accel: ");
    Serial.print(ax); Serial.print(", ");
    Serial.print(ay); Serial.print(", ");
    Serial.println(az);
    
    Serial.print("Gyro: ");
    Serial.print(gx); Serial.print(", ");
    Serial.print(gy); Serial.print(", ");
    Serial.println(gz);
  }
  
  delay(100);  // 10Hz update
}

Orientation Estimation

Simple orientation from accelerometer (pitch and roll only):
void calculateOrientation() {
  myICM.getAGMT();
  
  float ax = myICM.accX();
  float ay = myICM.accY();
  float az = myICM.accZ();
  
  // Calculate pitch and roll (radians)
  float pitch = atan2(-ax, sqrt(ay*ay + az*az));
  float roll = atan2(ay, az);
  
  // Convert to degrees
  pitch = pitch * 180.0 / PI;
  roll = roll * 180.0 / PI;
  
  Serial.print("Pitch: "); Serial.print(pitch);
  Serial.print(" Roll: "); Serial.println(roll);
}
Limitation: Accelerometer-only orientation doesn’t provide yaw (heading) and drifts during motion. Use gyroscope integration or magnetometer for yaw.

Complementary Filter for Orientation

Combine accelerometer (no drift, noisy) with gyroscope (drifts, smooth):
float pitch = 0, roll = 0, yaw = 0;
unsigned long lastTime = 0;
float alpha = 0.98;  // Filter coefficient

void updateOrientation() {
  myICM.getAGMT();
  
  unsigned long now = micros();
  float dt = (now - lastTime) / 1000000.0;  // seconds
  lastTime = now;
  
  // Gyroscope rates (deg/s)
  float gx = myICM.gyrX();
  float gy = myICM.gyrY();
  float gz = myICM.gyrZ();
  
  // Integrate gyroscope
  float pitchGyro = pitch + gx * dt;
  float rollGyro = roll + gy * dt;
  float yawGyro = yaw + gz * dt;
  
  // Accelerometer angles
  float ax = myICM.accX();
  float ay = myICM.accY();
  float az = myICM.accZ();
  
  float pitchAcc = atan2(-ax, sqrt(ay*ay + az*az)) * 180.0 / PI;
  float rollAcc = atan2(ay, az) * 180.0 / PI;
  
  // Complementary filter
  pitch = alpha * pitchGyro + (1 - alpha) * pitchAcc;
  roll = alpha * rollGyro + (1 - alpha) * rollAcc;
  yaw = yawGyro;  // Only gyro for yaw (or add magnetometer)
  
  Serial.print("Orientation - Pitch: ");
  Serial.print(pitch);
  Serial.print(" Roll: ");
  Serial.print(roll);
  Serial.print(" Yaw: ");
  Serial.println(yaw);
}

Calibration

Gyroscope Bias Calibration

Gyroscopes have offset even when stationary:
float gyro_bias_x = 0, gyro_bias_y = 0, gyro_bias_z = 0;

void calibrateGyro() {
  Serial.println("Calibrating gyroscope. Keep IMU still...");
  
  int samples = 1000;
  float sum_x = 0, sum_y = 0, sum_z = 0;
  
  for (int i = 0; i < samples; i++) {
    while (!myICM.dataReady()) {}
    myICM.getAGMT();
    
    sum_x += myICM.gyrX();
    sum_y += myICM.gyrY();
    sum_z += myICM.gyrZ();
    
    delay(5);
  }
  
  gyro_bias_x = sum_x / samples;
  gyro_bias_y = sum_y / samples;
  gyro_bias_z = sum_z / samples;
  
  Serial.println("Calibration complete!");
  Serial.print("Bias: ");
  Serial.print(gyro_bias_x); Serial.print(", ");
  Serial.print(gyro_bias_y); Serial.print(", ");
  Serial.println(gyro_bias_z);
}

// Use in orientation calculation:
float gx = myICM.gyrX() - gyro_bias_x;
float gy = myICM.gyrY() - gyro_bias_y;
float gz = myICM.gyrZ() - gyro_bias_z;

Magnetometer Calibration

Magnetometer needs hard-iron and soft-iron calibration. Complex topic - use existing tools or libraries.

Integration with Motor Control

Combine IMU with motor control for heading correction:
float targetHeading = 0;  // degrees
float currentHeading = 0; // from IMU yaw

void headingControl() {
  // Calculate heading error
  float headingError = targetHeading - currentHeading;
  
  // Normalize to -180 to +180
  while (headingError > 180) headingError -= 360;
  while (headingError < -180) headingError += 360;
  
  // Apply correction to rotation velocity
  float rotationCorrection = headingError * 0.01;  // P controller
  
  // Add to motor commands
  // (integrate with existing motor control)
}

I2C Scanner

If IMU not detected, scan I2C bus:
void scanI2C() {
  Serial.println("Scanning I2C bus...");
  
  for (byte address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    byte error = Wire.endTransmission();
    
    if (error == 0) {
      Serial.print("Device found at 0x");
      if (address < 16) Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  Serial.println("Scan complete.");
}
Expected: 0x68 or 0x69 for ICM-20948

Troubleshooting

Solutions:
  • Run I2C scanner
  • Check wiring (SDA, SCL, VCC, GND)
  • Verify 3.3V power (NOT 5V!)
  • Check pull-up resistors on I2C lines
  • Try different I2C address (0x68 vs 0x69)
Solutions:
  • Check if sensors enabled
  • Verify data ready flag before reading
  • Try different sensor range settings
  • Reset IMU and re-initialize
Solutions:
  • Enable DLPF (Digital Low-Pass Filter)
  • Average multiple samples
  • Check for vibration (mount IMU rigidly)
  • Lower cutoff frequency
Expected: Gyroscopes drift!Solutions:
  • Calibrate gyroscope bias at startup
  • Use sensor fusion (complementary or Kalman filter)
  • Periodically re-zero using magnetometer

ROS2 Integration Preview

In WP4, you’ll publish IMU data to ROS2:
// Publish to ROS2 (simplified)
sensor_msgs::Imu imu_msg;
imu_msg.angular_velocity.x = gx;
imu_msg.angular_velocity.y = gy;
imu_msg.angular_velocity.z = gz;
imu_msg.linear_acceleration.x = ax;
imu_msg.linear_acceleration.y = ay;
imu_msg.linear_acceleration.z = az;
This data feeds into robot_localization for sensor fusion with odometry!

Next Steps

References

[1] ICM-20948 Datasheet: https://invensense.tdk.com/products/motion-tracking/9-axis/icm-20948/ [2] SparkFun Library: https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary [3] Sensor Fusion: https://www.nxp.com/docs/en/application-note/AN3461.pdf