MCU & FPGA posicionamento MPU9250: como usar uma IMU 9 eixos e extrair dados realmente “úteis”

MPU9250: como usar uma IMU 9 eixos e extrair dados realmente “úteis”


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:

  1. Accel/Gyro (bloco principal) acessado direto pelo barramento do host (I²C ou SPI).
  2. 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):

  1. Leitura atômica de accel/gyro (idealmente usando burst read para garantir coerência temporal).
  2. Conversão de escala (LSB → unidades físicas).
  3. Compensação de bias (offset) e, se necessário, escala fina (ganho).
  4. Filtragem leve (LPF) para reduzir ruído sem destruir dinâmica.
  5. 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
  6. 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:

  1. Hard-iron: remover offset (a “nuvem” de pontos do mag não fica centrada no zero).
  2. 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.

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários
0
Adoraria saber sua opinião, comente.x