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
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):
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.
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);
}