Page 1 of 1

How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Thu Oct 02, 2025 7:45 pm
by theluciansm
Hello everyone,

I am working with an ESP32 and an ICM-45686 IMU.
My goal is to acquire accelerometer and gyroscope data at 1.024 kHz and send it to a PC via serial. On the PC side, I have a Python script that receives the data and saves it.

I would like to evaluate whether the sampling rate of the ESP32 is actually stable. To check this, I compared the ESP32 data with a commercial data acquisition system, both synchronized by a trigger.

In the figure attached:

Black: measurement from the commercial system

Red: measurement from the ESP32

Blue: difference between the two signals

The signal is a measurement of a periodic movement of an object with 10 Hz of frequency.

We can observe that the difference (blue) is not just a fixed offset, but varies over time: the ESP32 signal starts slightly delayed and then seems to get ahead of the reference signal.

πŸ‘‰ How should I interpret this behavior?
πŸ‘‰ Does this variation in the blue residual indicate that the ESP32 sampling rate is not stable (jitter), or could there be another explanation?

I accept any suggestions.
THanks!

The esp32 code is below (using arduino IDE):

Code: Select all

/*******************************************************************************************
 *  ICM-45686 – 1 kHz Acquisition + Laser Trigger                                         *
 *                                                                                        *
 *  FEATURES:                                                                             *
 *   β€’ Reads accelerometer and gyroscope signals from the ICM-45686 IMU at 1 kHz          *
 *   β€’ Sends the data over serial in tabular format                                       *
 *   β€’ Activates the laser (GPIO 17) immediately after button detection (GPIO 33)         *
 *                                                                                        *
 *  Author: Lucian Ribeiro da Silva – Jun/2025                                            *
 *******************************************************************************************/

#include <Arduino.h>
#include <SPI.h>
#include "ICM45686.h"
#include "soc/gpio_reg.h"  // For direct register manipulation (if needed)
#include "driver/gpio.h"
#include "soc/io_mux_reg.h"
#include "soc/soc.h"


/* ─────────────────────────────────────
 *  Application pin definitions
 * ───────────────────────────────────── */
constexpr int CS_PIN        = 5;    // IMU Chip Select on the SPI bus
constexpr int INT1_PIN      = 4;    // IMU interrupt pin (not used)
// constexpr int PINO_BOTAO    = 17;   // <-- REMOVED
constexpr int PINO_TRIGGER  = 33;   // Digital output to activate the laser
constexpr int PINO_TRIGGER_SCADAS  = 27;   // Digital trigger output for SCADAS
constexpr int DEBUG_PIN     = 2;    // On-board LED for debugging (optional)
constexpr int tempoHIGH = 15000;
constexpr int Fs_loop = 1024; // frequency of signal transmission via serial (not the sensor ODR, be careful)
int Ts = 0;
/* ─────────────────────────────────────
 *  Global variables
 * ───────────────────────────────────── */
int32_t off_ax, off_ay, off_az;     // Acceleration offsets
int32_t off_gx, off_gy, off_gz;     // Gyroscope offsets

ICM456xx IMU(SPI, CS_PIN);          // IMU object connected via SPI

volatile int triggerAtivado = 0;    // <-- MODIFIED: renamed from 'botaoPressionado'
unsigned long tempoAcionamento = 0; // Moment when the button was pressed

char buffer[512];                   // Buffer for serial output formatting

/* ─────────────────────────────────────
 *  Button interrupt // <-- REMOVED
 * ───────────────────────────────────── */
// void handleBotao() {
//     if (botaoPressionado == 0) {       // Ensures only the first click is registered
//         botaoPressionado = 1;
//         tempoAcionamento = millis();   // Saves the activation timestamp
//     }
// }

/* ─────────────────────────────────────
 *  System initial setup
 * ───────────────────────────────────── */
void setup() {
    Serial.begin(1500000);
    while (!Serial) {}

    // pinMode(PINO_BOTAO, INPUT_PULLDOWN); // <-- REMOVED
    // attachInterrupt(digitalPinToInterrupt(PINO_BOTAO), handleBotao, RISING); // <-- REMOVED

    pinMode(PINO_TRIGGER, OUTPUT);
    digitalWrite(PINO_TRIGGER, LOW);

    pinMode(PINO_TRIGGER_SCADAS, OUTPUT);
    digitalWrite(PINO_TRIGGER_SCADAS, LOW);

    pinMode(DEBUG_PIN, OUTPUT);
    digitalWrite(DEBUG_PIN, LOW);

    SPI.begin(18, 19, 23, CS_PIN);
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);

    pinMode(INT1_PIN, INPUT_PULLUP);

    if (IMU.begin() != 0) {
        Serial.println("# ERROR: IMU did not initialize");
        while (true) delay(1000);
    }

    IMU.startAccel(1600, 2);    // Β±2g
    IMU.startGyro (1600, 15);   // Β±15.625 Β°/s
    delay(50);

    int64_t sum[6] = {0};
    inv_imu_sensor_data_t d;
    for (int i = 0; i < 500; ++i) {
        IMU.getDataFromRegisters(d);
        sum[0] += d.accel_data[0];
        sum[1] += d.accel_data[1];
        sum[2] += d.accel_data[2];
        sum[3] += d.gyro_data[0];
        sum[4] += d.gyro_data[1];
        sum[5] += d.gyro_data[2];
        delayMicroseconds(1000);
    }
    off_ax = sum[0] / 500;
    off_ay = sum[1] / 500;
    off_az = sum[2] / 500;
    off_gx = sum[3] / 500;
    off_gy = sum[4] / 500;
    off_gz = sum[5] / 500;

    Serial.println("trigger\tax\tay\taz\tgx\tgy\tgz"); // <-- MODIFIED: updated header

    Ts = (1000000UL / Fs_loop);
}

/* ─────────────────────────────────────
 *  Main loop – runs at 1 kHz
 * ───────────────────────────────────── */
void loop() {
    static uint32_t nextSampleTime = micros() + Ts;
    
    // Busy-wait until the exact sampling moment
    while (micros() < nextSampleTime) {
        // Small pause to reduce CPU usage
        __asm__ volatile("nop");
    }
    
    // Update the next sample time WITHOUT accumulating error
    nextSampleTime += Ts;
    
    // Check if we are behind schedule (drift protection)
    if (micros() > nextSampleTime) {
        // If we are too late, resynchronize
        nextSampleTime = micros() + Ts;
    }
// --- ADDED: Python command "listener" ---
if (Serial.available() > 0 && triggerAtivado == 0) {
    int comando = Serial.parseInt();
   if (comando == 1) {
        triggerAtivado = 1;
        digitalWrite(PINO_TRIGGER, HIGH);
        digitalWrite(PINO_TRIGGER_SCADAS, HIGH);
        //REG_WRITE(GPIO_OUT_W1TS_REG, (1ULL << PINO_TRIGGER) | (1ULL << PINO_TRIGGER_SCADAS));

    }
}


    // IMU reading
    inv_imu_sensor_data_t d;
    IMU.getDataFromRegisters(d);

    int32_t ax = d.accel_data[0] - off_ax;
    int32_t ay = d.accel_data[1] - off_ay;
    int32_t az = d.accel_data[2] - off_az;
    int32_t gx = d.gyro_data[0]  - off_gx;
    int32_t gy = d.gyro_data[1]  - off_gy;
    int32_t gz = d.gyro_data[2]  - off_gz;

    snprintf(buffer, sizeof(buffer),
             "%d\t%ld\t%ld\t%ld\t%ld\t%ld\t%ld",
             triggerAtivado, ax, ay, az, gx, gy, gz);
    Serial.println(buffer);
}


Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Fri Oct 03, 2025 9:13 am
by Sprite
The error is fairly small - I'm eyeballing that at the end the amplitude is about 1/3rd of the actual waveforms, meaning it leads or lags by (sin-1(0.333)/2*pi=)0.04 cycles. On a total of 80 cycles, that's about 0.05%. That is probably be explained by the fact that you're using ints for Ts, nextSampleTime etc - your Ts should be (1000000/1024)=976.562 but because you store it as an int, it's 976. That leads to an actual frequency of (1000000/976)=1024.59Hz, which indeed is off by 0.05%.

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Fri Oct 03, 2025 2:27 pm
by theluciansm
The error is fairly small - I'm eyeballing that at the end the amplitude is about 1/3rd of the actual waveforms, meaning it leads or lags by (sin-1(0.333)/2*pi=)0.04 cycles. On a total of 80 cycles, that's about 0.05%. That is probably be explained by the fact that you're using ints for Ts, nextSampleTime etc - your Ts should be (1000000/1024)=976.562 but because you store it as an int, it's 976. That leads to an actual frequency of (1000000/976)=1024.59Hz, which indeed is off by 0.05%.
Hi @Sprite, thanks a lot for your explanation.
That was a very good observation β€” I hadn’t considered the effect of using int for Ts and nextSampleTime. I’ll update the code to use the correct precision and then come back here to share the new results.

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Fri Oct 03, 2025 3:20 pm
by theluciansm
The error is fairly small - I'm eyeballing that at the end the amplitude is about 1/3rd of the actual waveforms, meaning it leads or lags by (sin-1(0.333)/2*pi=)0.04 cycles. On a total of 80 cycles, that's about 0.05%. That is probably be explained by the fact that you're using ints for Ts, nextSampleTime etc - your Ts should be (1000000/1024)=976.562 but because you store it as an int, it's 976. That leads to an actual frequency of (1000000/976)=1024.59Hz, which indeed is off by 0.05%.
Hi @Sprite, thanks a lot for your explanation.
That was a very good observation β€” I hadn’t considered the effect of using int for Ts and nextSampleTime. I’ll update the code to use the correct precision and then come back here to share the new results.
I changed for the following code

Code: Select all



/***********
 *  ICM-45686 – AquisiΓ§Γ£o a 1 kHz + Disparo de Laser
 *
 *  CorreΓ§Γ£o: uso de double para Ts e uint64_t para temporizaΓ§Γ£o,
 *  evitando truncamento (976 vs 976.562 Β΅s) que gerava drift.
 *
 *  Autor: Lucian Ribeiro da Silva – Rev. Out/2025
 ***********/

#include <Arduino.h>
#include <SPI.h>
#include "ICM45686.h"
#include "soc/gpio_reg.h"
#include "driver/gpio.h"
#include "soc/io_mux_reg.h"
#include "soc/soc.h"

/* ─────────────────────────────────────
 *  DefiniΓ§Γ΅es de pinos da aplicaΓ§Γ£o
 * ───────────────────────────────────── */
constexpr int CS_PIN        = 5;
constexpr int INT1_PIN      = 4;
constexpr int PINO_TRIGGER  = 33;
constexpr int PINO_TRIGGER_SCADAS = 27;
constexpr int DEBUG_PIN     = 2;

constexpr double Fs_loop = 1024.0;   // FrequΓͺncia de envio (Hz)
double Ts;                           // PerΓ­odo em microssegundos (preciso)
                                     
int32_t off_ax, off_ay, off_az;
int32_t off_gx, off_gy, off_gz;

ICM456xx IMU(SPI, CS_PIN);

volatile int triggerAtivado = 0;
unsigned long tempoAcionamento = 0;

char buffer[128];

/* ─────────────────────────────────────
 *  Setup
 * ───────────────────────────────────── */
void setup() {
    Serial.begin(1500000);
    while (!Serial) {}

    pinMode(PINO_TRIGGER, OUTPUT);
    digitalWrite(PINO_TRIGGER, LOW);

    pinMode(PINO_TRIGGER_SCADAS, OUTPUT);
    digitalWrite(PINO_TRIGGER_SCADAS, LOW);

    pinMode(DEBUG_PIN, OUTPUT);
    digitalWrite(DEBUG_PIN, LOW);

    SPI.begin(18, 19, 23, CS_PIN);
    pinMode(CS_PIN, OUTPUT);
    digitalWrite(CS_PIN, HIGH);

    pinMode(INT1_PIN, INPUT_PULLUP);

    if (IMU.begin() != 0) {
        Serial.println("# ERRO: IMU nΓ£o inicializou");
        while (true) delay(1000);
    }

    IMU.startAccel(1600, 2);    // Β±2g
    IMU.startGyro (1600, 15);   // Β±15,625Β°/s
    delay(50);

    int64_t sum[6] = {0};
    inv_imu_sensor_data_t d;
    for (int i = 0; i < 500; ++i) {
        IMU.getDataFromRegisters(d);
        sum[0] += d.accel_data[0];
        sum[1] += d.accel_data[1];
        sum[2] += d.accel_data[2];
        sum[3] += d.gyro_data[0];
        sum[4] += d.gyro_data[1];
        sum[5] += d.gyro_data[2];
        delayMicroseconds(1000);
    }
    off_ax = sum[0] / 500;
    off_ay = sum[1] / 500;
    off_az = sum[2] / 500;
    off_gx = sum[3] / 500;
    off_gy = sum[4] / 500;
    off_gz = sum[5] / 500;

    Serial.println("trigger\tax\tay\taz\tgx\tgy\tgz");

    // Agora Ts Γ© armazenado com precisΓ£o em double
    Ts = 1000000.0 / Fs_loop; // ~976.562 Β΅s
}

/* ─────────────────────────────────────
 *  Loop principal – executa a 1 kHz
 * ───────────────────────────────────── */
void loop() {
    static uint64_t nextSample = micros();  // uint64_t para evitar overflow

    // espera atΓ© o tempo exato
    while (micros() < nextSample) {}
    nextSample += (uint64_t)Ts;  // incremento com precisΓ£o em double

    // --- Ouvinte de comando via Serial ---
    if (Serial.available() > 0 && triggerAtivado == 0) {
        int comando = Serial.parseInt();
        if (comando == 1) {
            triggerAtivado = 1;
            digitalWrite(PINO_TRIGGER, HIGH);
            digitalWrite(PINO_TRIGGER_SCADAS, HIGH);
        }
    }

    // Leitura da IMU
    inv_imu_sensor_data_t d;
    IMU.getDataFromRegisters(d);

    int32_t ax = d.accel_data[0] - off_ax;
    int32_t ay = d.accel_data[1] - off_ay;
    int32_t az = d.accel_data[2] - off_az;
    int32_t gx = d.gyro_data[0]  - off_gx;
    int32_t gy = d.gyro_data[1]  - off_gy;
    int32_t gz = d.gyro_data[2]  - off_gz;

    snprintf(buffer, sizeof(buffer),
             "%d\t%ld\t%ld\t%ld\t%ld\t%ld\t%ld",
             triggerAtivado, ax, ay, az, gx, gy, gz);
    Serial.println(buffer);
}
But I’m still observing a phase shift between the signals.
I attached a screenshot from the beginning and the end of the measurement.
Could the error be due to sending a large amount of data through the serial interfaceβ€”in other words, could the serial communication be bottlenecking the system? The sensor’s ODR is set to 1600, since it cannot be configured to 1024. Could this also be causing some delay or advance in the signals?

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Sat Oct 04, 2025 12:37 am
by Sprite
No real use making Ts a double if you then go cast it to an int anyway:

Code: Select all

nextSample += (uint64_t)Ts
Suggest you make nextSample a double as well.

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Mon Oct 06, 2025 11:23 am
by esp_man
The sensor’s ODR is set to 1600, since it cannot be configured to 1024. Could this also be causing some delay or advance in the signals?
If the sensor is sample rate generator itself, it may cause errors. But not delay or advance, but the sample rate variations. The delay or advance is only a further consequence.
It is typical for many MEMS devices that the Output Data Rate is generated by internal RC oscillator. Therefore, it is inherently of low accuracy.

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Mon Oct 06, 2025 12:45 pm
by theluciansm
No real use making Ts a double if you then go cast it to an int anyway:

Code: Select all

nextSample += (uint64_t)Ts
Suggest you make nextSample a double as well.
Hi @Sprite,
Thanks again for your help.
I’ve modified my code, and now I’m able to synchronize both signals (ESP vs. commercial system).
I’m attaching the signals below.
Thank you very much for your support!

Re: How to achieve stable sampling rate (1 kHz) when reading IMU data and sending over Serial on ESP32?

Posted: Tue Oct 07, 2025 1:12 am
by Sprite
Glad to hear you made it work!