Issue removing gravity from calibrated IMU acceleration (ICM-20948 + ESP32)

Hi everyone,

I’m working on a project using an ESP32 with two ICM-20948 IMUs to measure paddle motion. I’ve implemented a full 6-pose accelerometer calibration and I’m logging the following data at ~80 Hz:

  • Calibrated accelerometer (after bias + scale)

  • Calibrated gyroscope

  • Quaternions (from the ICM-20948, no magnetometer)

Later, in MATLAB, I rotate the acceleration into the global frame using the quaternions and remove gravity. However, the results still don’t look correct, and I’m now trying to understand whether the issue comes from my Arduino acquisition code or from the MATLAB processing stage.

Before I post on MATLAB Answers, I’d like to confirm with you if my data acquisition pipeline looks correct and whether I’m handling the calibrated acceleration properly at firmware level.


:wrench: What I do on the ESP32

  • Read accel & gyro using the ICM20948_WE library

  • Apply my calibration parameters (bias + gain) obtained from a 6-pose routine

  • Log calibrated accel (in g), calibrated gyro, and quaternions

  • No magnetometer is involved at this stage

Below is a reduced version of the code, showing only the important parts (IMU setup, calibration application, data read & logging). I removed the webserver UI and SD details to keep it short:

// [...] shortened sample – includes: setupIMU(), reading IMU, applying bias & gain, logging data

// Example of how accel is corrected before logging:
acc.x = (acc.x - A_BIAS_x) * A_GAIN_x;
acc.y = (acc.y - A_BIAS_y) * A_GAIN_y;
acc.z = (acc.z - A_BIAS_z) * A_GAIN_z;

// Similar for gyro

// Quaternions taken directly from the IMU library (normalized before sending)

// Logged format per sample:
// q0,q1,q2,q3, ax,ay,az, gx,gy,gz

(If helpful, I can share the full code file.)


:red_question_mark: My questions

  1. Does this acquisition pipeline appear correct for later removal of gravity using quaternions?
    (i.e., apply accel calibration on the ESP32, then rotate to global frame & subtract gravity in MATLAB)

  2. Is there anything missing or commonly recommended to do before logging the calibrated acceleration?
    e.g., re-ordering quaternion components, additional filtering, converting to m/s² instead of g before logging, etc.

  3. Since I’m not using the magnetometer (on purpose), I know yaw will drift over time —
    but for short recordings, is logging the IMU quaternion still reliable enough for gravity removal and linear acceleration estimation?

Any advice is appreciated — I’ve spent quite a bit of time validating both firmware and MATLAB processing, and I want to ensure the data I’m logging is correct before proceeding with further debugging.

Thanks in advance!

With consumer grade IMUs, the errors introduced by the attempt to subtract the gravity component are usually so large as to make the result (the so called "linear acceleration") useless for further analysis, like motion tracking.

This is largely due to the inaccuracy with which the 3D orientation of the sensor is known, as explained in this archived article.

The topic comes up fairly often on this forum, and in several years, I've never seen anyone report success.

Thanks — I fully agree about the limitations for long-term tracking with consumer IMUs.

Just to clarify my use case: I’m not after long-term 3D tracking or position. I’m mounting an IMU on the paddle shaft and I only need short-window linear acceleration per stroke (and possibly short-window velocity), so I can analyse features within each paddling stroke. Given this scope, I’d like to make sure my acquisition pipeline and firmware code are appropriate.

What I do now (ESP32 + ICM-20948):

  • Read accel/gyro via library, apply 6-pose accel calibration (bias + scale) on-device

  • Log calibrated accel (g), calibrated gyro, and quaternions (no magnetometer) at ~80 Hz

  • In MATLAB: normalize quaternions → rotate accel to global → subtract g to get linear acceleration

Could you suggest concrete improvements to the firmware for this short-window goal? For example:

  1. Filtering: low-pass accel before rotation, or after? Typical cut-offs you recommend?

  2. Quaternion care: always normalize each sample? any best practices with the ICM-20948 quats?

  3. Units/format: log accel in m/s² instead of g? fixed-point vs float for stability?

  4. Sampling: is ~80 Hz enough for paddle strokes, or would you push to 100–200 Hz?

  5. Calibration: anything else I should apply on-device beyond bias + scale (e.g., soft-iron-like scale matrix)?

  6. Robustness: timestamps, buffering, ordering — any pitfalls you often see that hurt accuracy?

If you spot obvious improvements to the code structure for this purpose, I’d really appreciate pointers. My goal is a robust short-window linear-acceleration estimate per stroke, fully aware of the usual drift limits for long durations.

Thanks in advance!

Since the 3D orientation of the IMU is continually changing during a paddle stroke, that sounds like a extremely challenging task. Especially so using only 6 axes of the 9DOF sensor.

What experimental tests have you done to validate the idea?

As an alternative, I've seen reports of people attaching strain gauges to paddle shafts and successfully measuring the forces.

thanks for your message — and you’re right that fully reconstructing paddle motion with only 6 axes of a 9DOF module is challenging, especially if the goal is long-term tracking without drift. I definitely don’t expect to solve that with naïve double integration.

Just to clarify the approach I’m following:

The final goal is indeed to extract meaningful paddle kinematics (orientation and short-window velocity/trajectory over strokes). I’m aware this requires careful processing and constraints to control drift.

For that reason, I’m building this in stages, and I’m currently focusing only on Stage 1, which is much more limited:

Stage 1 (current focus):

• Obtain the cleanest possible calibrated accelerometer + gyro + quaternion data

• Convert to global frame and perform gravity removal reliably

• Validate this step on an ergometer against a motion-capture reference before moving forward

From feedback I got on the MATLAB side, the main bottleneck right now is data quality before processing (residual bias/scale/misalignment). So before I move to more ambitious steps, I want to ensure that the firmware acquisition pipeline is solid.

That’s why I’m looking for advice specifically on the firmware stage — things like good practices for:

• accel/gyro calibration (bias/scale vs full 3×3 matrix)

• quaternion “hygiene” (normalisation, handling of W-first format, etc.)

• filtering accel before rotation (or not)

• sampling considerations for ~80–100 Hz stroke motion

Once I have high-quality signals at the acquisition stage, the next step will be to apply drift-limiting techniques in short windows (e.g., per-stroke integration with constraints), but I don’t expect the IMU alone to give full long-term position.

And yes — I am already using strain gauges on the paddle (separate module) to measure forces; the IMU is only for kinematics.

So at this stage, any guidance you have specifically on improving the IMU acquisition firmware to get the most stable accel/gyro/quat data would be a big help

That is the sticking point. Even with a 9DOF, carefully calibrated, consumer grade IMU, converting to the fixed frame with the accuracy that the project requires is simply not possible today, nor in the foreseeable future. With a 6DOF IMU the prospects are much worse.

It is possible with the professional grade IMUs used in aircraft navigation, but because this is far from a simple problem, they cost in the general range of US $20K-250K.

You are certainly welcome to disagree, but unless you can demonstrate otherwise, your project will not proceed past this point. In any case, have fun!