O MPU9250 ficou popular porque entrega, num único encapsulamento, um acelerômetro de 3 eixos, um giroscópio de 3 eixos e um magnetômetro de 3 eixos (na prática, são dois “dies”: o bloco accel/gyro e o AK8963 para bússola). Isso permite estimar orientação 3D (roll, pitch, yaw), detectar padrões de movimento, vibração e eventos transitórios, além de fornecer insumos para navegação inercial e estabilização. Ele é muito bom para aprendizado e prototipagem, mas hoje está marcado como fim de vida/NRND pela própria TDK/InvenSense, então a recomendação é não projetar produto novo em cima dele. (TDK InvenSense)

A parte “enganosa” das IMUs é que ler registradores e imprimir “Ax, Ay, Az” quase nunca resolve o problema real. O valor está cru, com bias (offset), ruído, drift, saturação, escalas diferentes, e no caso do magnetômetro ainda existe distorção de hard-iron/soft-iron. O objetivo aqui é te mostrar um caminho de uso que funciona na prática: inicialização correta, aquisição consistente, calibração e, por fim, fusão de sensores para produzir orientação e métricas refinadas.
1) Arquitetura do MPU9250 e o que isso muda no firmware
O MPU9250 tem dois “mundos” que você precisa coordenar:
- Accel/Gyro (bloco principal) acessado direto pelo barramento do host (I²C ou SPI).
- Magnetômetro (AK8963) acessado de duas formas: ou você coloca o MPU em “pass-through / bypass” e fala direto com o AK8963 no endereço I²C 0x0C, ou você usa o barramento auxiliar interno do MPU (modo “master”) para o MPU ler o magnetômetro e disponibilizar os dados ao host. A própria especificação descreve o uso do pass-through e cita explicitamente o endereço do AK8963. (TDK InvenSense)
Para simplificar e reduzir “magia” no início, muitos projetos usam pass-through: o host configura e lê o magnetômetro como se fosse outro dispositivo I²C no mesmo barramento. Isso também facilita depuração com ferramentas como i2cdetect, embora existam armadilhas de configuração.
2) O “pipeline” certo: do dado cru ao dado refinado
Um pipeline robusto costuma seguir esta lógica (mesmo que você implemente em etapas):
- Leitura atômica de accel/gyro (idealmente usando burst read para garantir coerência temporal).
- Conversão de escala (LSB → unidades físicas).
- Compensação de bias (offset) e, se necessário, escala fina (ganho).
- Filtragem leve (LPF) para reduzir ruído sem destruir dinâmica.
- Fusão de sensores para estimar orientação:
- Complementary filter (simples e bom) para roll/pitch
- Madgwick/Mahony (quaternions) para roll/pitch/yaw com magnetômetro
- Calibração do magnetômetro (hard-iron/soft-iron) para yaw confiável.
A ideia é: se você pular a calibração e fusão, yaw vai “dançar”, gyro vai derivar e vibração vira um festival de falso-positivo.
3) Base de firmware em C: leitura, escala e filtros
Abaixo vai um esqueleto que separa responsabilidades: driver (registradores) e processamento (algoritmos). Eu vou deixar as funções i2c_read e i2c_write como “portáveis” (você adapta para STM32 HAL, ESP-IDF, baremetal etc.).
3.1 Tipos e constantes úteis
#include <stdint.h>
#include <stdbool.h>
#include <math.h>
/* Endereços I2C típicos do MPU9250: 0x68 (AD0=0) ou 0x69 (AD0=1). */
#define MPU9250_ADDR 0x68
/* Magnetômetro AK8963 no pass-through/bypass: 0x0C */
#define AK8963_ADDR 0x0C /* citado na especificação do MPU9250 :contentReference[oaicite:2]{index=2} */
/* Exemplo de faixas (você escolhe conforme aplicação) */
typedef enum {
ACC_FS_2G = 0,
ACC_FS_4G,
ACC_FS_8G,
ACC_FS_16G
} acc_fs_t;
typedef enum {
GYR_FS_250DPS = 0,
GYR_FS_500DPS,
GYR_FS_1000DPS,
GYR_FS_2000DPS
} gyr_fs_t;
typedef struct {
float ax, ay, az; /* m/s^2 */
float gx, gy, gz; /* rad/s */
float mx, my, mz; /* uT (ou unidade relativa, depende do seu fator) */
float temp_c;
uint32_t t_us; /* timestamp (opcional) */
} imu_sample_t;
typedef struct {
/* Bias estimado (offset) */
float acc_bias[3];
float gyr_bias[3];
float mag_bias[3];
/* Matriz 3x3 de correção do magnetômetro (soft-iron) */
float mag_softiron[3][3];
/* Fatores de escala em unidades físicas por LSB */
float acc_lsb_to_mps2;
float gyr_lsb_to_rads;
float mag_lsb_to_uT; /* depende do modo do AK8963 e do datasheet dele */
} imu_cal_t;
3.2 Funções “portáveis” de barramento
/**
* @brief Lê N bytes de um registrador.
* @return true se sucesso.
*/
bool i2c_read(uint8_t dev, uint8_t reg, uint8_t *buf, uint16_t len);
/**
* @brief Escreve 1 byte em um registrador.
*/
bool i2c_write(uint8_t dev, uint8_t reg, uint8_t value);
4) Inicialização prática: o mínimo que evita dor de cabeça
Aqui entram três pontos: clock, ranges e habilitar leitura coerente. Para o magnetômetro via pass-through, você também precisa habilitar o bypass no MPU e configurar o AK8963. O documento de especificação descreve o “pass-through mode” justamente para permitir que o host acesse o AK8963 diretamente. (TDK InvenSense)
Observação importante: os endereços de registradores (PWR_MGMT_1, INT_PIN_CFG etc.) ficam no register map do MPU9250. Se você já usa uma lib, ótimo; se não, use o RM oficial para confirmar endereços e bits. (TDK InvenSense)
Exemplo (conceitual, com registradores “nomeados” via defines que você tiraria do RM):
/* Estes defines você deve mapear pelo Register Map oficial. :contentReference[oaicite:5]{index=5} */
#define REG_PWR_MGMT_1 0x6B
#define REG_PWR_MGMT_2 0x6C
#define REG_CONFIG 0x1A
#define REG_GYRO_CONFIG 0x1B
#define REG_ACCEL_CONFIG 0x1C
#define REG_ACCEL_CONFIG2 0x1D
#define REG_INT_PIN_CFG 0x37
#define REG_USER_CTRL 0x6A
/* Bits típicos: consultar RM */
#define BIT_BYPASS_EN 0x02 /* INT_PIN_CFG */
#define BIT_I2C_MST_EN 0x20 /* USER_CTRL (se usar master interno) */
static bool mpu9250_init_basic(acc_fs_t acc_fs, gyr_fs_t gyr_fs)
{
/* 1) Acorda o chip e seleciona clock (ex.: PLL) */
if (!i2c_write(MPU9250_ADDR, REG_PWR_MGMT_1, 0x01)) return false;
/* 2) Configura filtros digitais (DLPF) conforme seu fs e ruído */
if (!i2c_write(MPU9250_ADDR, REG_CONFIG, 0x03)) return false;
/* 3) Faixa do giroscópio */
uint8_t gcfg = (uint8_t)(gyr_fs << 3);
if (!i2c_write(MPU9250_ADDR, REG_GYRO_CONFIG, gcfg)) return false;
/* 4) Faixa do acelerômetro */
uint8_t acfg = (uint8_t)(acc_fs << 3);
if (!i2c_write(MPU9250_ADDR, REG_ACCEL_CONFIG, acfg)) return false;
/* 5) Config do accel DLPF */
if (!i2c_write(MPU9250_ADDR, REG_ACCEL_CONFIG2, 0x03)) return false;
return true;
}
/* Habilita pass-through para acessar AK8963 direto no I2C (0x0C). :contentReference[oaicite:6]{index=6} */
static bool mpu9250_enable_mag_passthrough(void)
{
/* Desabilita master interno (se estiver ligado) e ativa bypass */
if (!i2c_write(MPU9250_ADDR, REG_USER_CTRL, 0x00)) return false;
if (!i2c_write(MPU9250_ADDR, REG_INT_PIN_CFG, BIT_BYPASS_EN)) return false;
return true;
}
5) Aquisição coerente: burst read e conversão para unidades físicas
A leitura ideal é em “bloco”, porque isso reduz a chance de pegar metade do sample antigo e metade do novo (especialmente quando você lê eixos separados).
#define REG_ACCEL_XOUT_H 0x3B /* base do bloco accel/temp/gyro no RM */
/* Layout típico do burst: accel(6), temp(2), gyro(6) => 14 bytes */
static bool mpu9250_read_accel_gyro_raw(int16_t acc[3], int16_t gyr[3], int16_t *temp)
{
uint8_t buf[14];
if (!i2c_read(MPU9250_ADDR, REG_ACCEL_XOUT_H, buf, 14)) return false;
acc[0] = (int16_t)((buf[0] << 8) | buf[1]);
acc[1] = (int16_t)((buf[2] << 8) | buf[3]);
acc[2] = (int16_t)((buf[4] << 8) | buf[5]);
*temp = (int16_t)((buf[6] << 8) | buf[7]);
gyr[0] = (int16_t)((buf[8] << 8) | buf[9]);
gyr[1] = (int16_t)((buf[10] << 8) | buf[11]);
gyr[2] = (int16_t)((buf[12] << 8) | buf[13]);
return true;
}
/* Converte valores crus para físico, já aplicando bias */
static void imu_convert_apply_cal(
const int16_t acc_raw[3],
const int16_t gyr_raw[3],
const imu_cal_t *cal,
float acc_mps2[3],
float gyr_rads[3]
){
/* LSB -> unidade e remove bias */
for (int i = 0; i < 3; i++) {
acc_mps2[i] = (float)acc_raw[i] * cal->acc_lsb_to_mps2 - cal->acc_bias[i];
gyr_rads[i] = (float)gyr_raw[i] * cal->gyr_lsb_to_rads - cal->gyr_bias[i];
}
}
6) Calibração essencial: bias do giroscópio e “leveling” do acelerômetro
6.1 Bias do giroscópio (parado)
O gyro sempre tem um offset. Se você integrar gyro “cru”, a orientação deriva mesmo com o sensor parado. O método mais robusto para começar é: durante 2–5 s com o dispositivo imóvel, calcule a média de Gx,Gy,Gz e chame isso de bias.
static void calibrate_gyro_bias(imu_cal_t *cal, int samples, float dt_s)
{
(void)dt_s; /* aqui dt não é necessário, mas você pode checar estabilidade */
double sum[3] = {0,0,0};
for (int n = 0; n < samples; n++) {
int16_t acc_raw[3], gyr_raw[3], temp_raw;
if (!mpu9250_read_accel_gyro_raw(acc_raw, gyr_raw, &temp_raw)) continue;
sum[0] += (double)gyr_raw[0];
sum[1] += (double)gyr_raw[1];
sum[2] += (double)gyr_raw[2];
}
cal->gyr_bias[0] = (float)(sum[0]/samples) * cal->gyr_lsb_to_rads;
cal->gyr_bias[1] = (float)(sum[1]/samples) * cal->gyr_lsb_to_rads;
cal->gyr_bias[2] = (float)(sum[2]/samples) * cal->gyr_lsb_to_rads;
}

Para o acelerômetro, o bias depende muito de como você define “referência”: em repouso, ele mede gravidade projetada no frame do sensor. Um jeito prático: faça uma calibração simples em repouso com o sensor em uma orientação conhecida (por exemplo, Z apontando para cima), e ajuste apenas o eixo da gravidade. Para calibração “de verdade”, você faz a técnica de 6 faces (±X, ±Y, ±Z), mas isso já vira um procedimento de bancada.
7) Filtro passa-baixa simples (IIR) e por que ele é suficiente em muita coisa
Em IMU, um filtro IIR de 1ª ordem é o “feijão com arroz” porque custa quase nada e reduz ruído.
typedef struct {
float y[3];
float alpha; /* 0..1 (quanto menor, mais filtrado) */
} lpf1_t;
static void lpf1_init(lpf1_t *f, float alpha)
{
f->alpha = alpha;
f->y[0] = f->y[1] = f->y[2] = 0.0f;
}
static void lpf1_update(lpf1_t *f, const float x[3], float out[3])
{
for (int i = 0; i < 3; i++) {
f->y[i] = f->alpha * f->y[i] + (1.0f - f->alpha) * x[i];
out[i] = f->y[i];
}
}
Escolha de alpha: se seu loop é 200 Hz, alpha entre 0,85 e 0,98 costuma funcionar bem para suavizar sem “matar” dinâmica. Para vibração e manutenção preditiva, você pode filtrar menos (alpha menor) e trabalhar com FFT/PSD em janelas curtas.
8) Orientação “refinada”: Complementary Filter (rápido e bom) e caminho para quaternions
8.1 Roll e Pitch com Complementary Filter
O acelerômetro dá uma boa referência de inclinação (porque a gravidade aponta “para baixo”), mas é ruidoso e sofre com acelerações lineares. O gyro é suave e rápido, mas deriva. O filtro complementar mistura os dois: gyro domina no curto prazo, accel corrige no longo prazo.
typedef struct {
float roll; /* rad */
float pitch; /* rad */
float alpha; /* ex.: 0.98 */
} comp_t;
static void comp_init(comp_t *c, float alpha)
{
c->roll = 0.0f;
c->pitch = 0.0f;
c->alpha = alpha;
}
static void comp_update(comp_t *c, const float acc[3], const float gyr[3], float dt)
{
/* roll_acc = atan2(Ay, Az) ; pitch_acc = atan2(-Ax, sqrt(Ay^2+Az^2)) */
float roll_acc = atan2f(acc[1], acc[2]);
float pitch_acc = atan2f(-acc[0], sqrtf(acc[1]*acc[1] + acc[2]*acc[2]));
/* Integra gyro (assumindo gyr[] em rad/s) */
float roll_g = c->roll + gyr[0]*dt;
float pitch_g = c->pitch + gyr[1]*dt;
/* Mistura */
c->roll = c->alpha*roll_g + (1.0f - c->alpha)*roll_acc;
c->pitch = c->alpha*pitch_g + (1.0f - c->alpha)*pitch_acc;
}

Isso já resolve uma quantidade enorme de aplicações: estabilização simples, detecção de inclinação, controle de plataforma, etc. Mas ainda falta yaw (rumo).
8.2 Yaw e magnetômetro: onde quase todo mundo sofre
Yaw com magnetômetro só fica bom se você fizer duas coisas:
- Hard-iron: remover offset (a “nuvem” de pontos do mag não fica centrada no zero).
- Soft-iron: corrigir escala/rotação (a nuvem vira uma elipse em vez de esfera).
O procedimento prático é coletar dados do magnetômetro girando o dispositivo em várias orientações, ajustar uma elipse e transformar para esfera. Em firmware embarcado simples, você pode aplicar uma aproximação: bias = (max+min)/2 por eixo e escala = (max-min)/2 para normalizar. Não é perfeito, mas melhora muito.
Aplicação de correção (bias + matriz soft-iron):
static void mag_apply_cal(const imu_cal_t *cal, const float m_raw[3], float m_corr[3])
{
float v[3] = {
m_raw[0] - cal->mag_bias[0],
m_raw[1] - cal->mag_bias[1],
m_raw[2] - cal->mag_bias[2]
};
/* m_corr = SoftIron * v */
for (int r = 0; r < 3; r++) {
m_corr[r] =
cal->mag_softiron[r][0]*v[0] +
cal->mag_softiron[r][1]*v[1] +
cal->mag_softiron[r][2]*v[2];
}
}
Depois disso, para obter yaw “tilt-compensated” (compensado por roll/pitch), você projeta o vetor magnético no plano horizontal do mundo (ou do corpo) usando a orientação atual. Se você já estiver em quaternions (Madgwick/Mahony), isso fica limpo. Se você estiver em roll/pitch, dá para fazer com trigonometria também, mas o ideal para robustez é migrar para quaternion.
9) Leitura do magnetômetro (pass-through) e sincronismo
Quando você usa pass-through, o host lê o AK8963 em 0x0C diretamente, o que é exatamente o que o modo foi feito para permitir. (TDK InvenSense) A atenção aqui é garantir que você está lendo no ritmo correto e respeitando o “data ready” do magnetômetro (senão você repete amostras).
Um pseudo-exemplo (registradores do AK8963 você pega do datasheet/register map do AK):
/* Endereços do AK8963 (exemplo comum) */
#define AK8963_ST1 0x02
#define AK8963_HXL 0x03
#define AK8963_CNTL1 0x0A
static bool ak8963_init_16bit_100hz(void)
{
/* Exemplo: modo contínuo 100Hz e saída 16-bit (depende do CNTL1) */
return i2c_write(AK8963_ADDR, AK8963_CNTL1, 0x16);
}
static bool ak8963_read_raw(int16_t mag[3])
{
uint8_t st1;
if (!i2c_read(AK8963_ADDR, AK8963_ST1, &st1, 1)) return false;
if (!(st1 & 0x01)) {
/* dado ainda não pronto */
return false;
}
uint8_t buf[7];
if (!i2c_read(AK8963_ADDR, AK8963_HXL, buf, 7)) return false;
/* Atenção: AK8963 costuma ser little-endian */
mag[0] = (int16_t)((buf[1] << 8) | buf[0]);
mag[1] = (int16_t)((buf[3] << 8) | buf[2]);
mag[2] = (int16_t)((buf[5] << 8) | buf[4]);
return true;
}
10) Nota de engenharia: NRND/EOL e o que fazer com isso
A TDK/InvenSense marca o MPU-9250 como fim de vida / não recomendado para novos projetos, então em produto final vale considerar sucessores (ex.: ICM-20948 e outros, dependendo do que você precisa). (TDK InvenSense) Para aprendizado, laboratório, hobby e protótipos, ele continua sendo excelente justamente porque existe muito material, exemplos e bibliotecas.
Exemplos de gráficos em Python (no final, como referência)
Abaixo está um script didático que gera dados sintéticos (simulados) de aceleração, giroscópio, magnetômetro e também a orientação (Euler) usada como referência do exemplo. Ele serve para você enxergar a “cara” típica dos eixos e como eles variam no tempo. É útil para validar pipeline, filtros e escalas antes de plugar no hardware.