Giroscópio na prática: taxa de rotação, integração no tempo e ângulo estável com fusão (filtro complementar)
O giroscópio do MPU6050 mede velocidade angular em cada eixo, tipicamente em graus por segundo (°/s) depois que você converte usando o fator de escala correto. Diferente do acelerômetro, que “vê” a gravidade e por isso entrega uma referência absoluta de inclinação quando o movimento é suave, o giroscópio é excelente para captar mudanças rápidas de orientação. O ponto crítico é que o giroscópio tem bias (offset) e ruído; quando você integra ( \omega(t) ) no tempo para obter ângulo, qualquer pequeno bias vira uma deriva que cresce continuamente.
A integração é conceitualmente simples: se \( \omega \) é a taxa em °/s e \( \Delta t \) é o tempo entre amostras em segundos, então \( \Delta \theta \approx \omega \cdot \Delta t \). O desafio real é garantir \( \Delta t \) bem medido, compensar o bias e, para ficar robusto, “puxar” o ângulo de volta usando o acelerômetro como referência lenta. É exatamente isso que um filtro complementar faz: passa-alta no giroscópio (movimento rápido) e passa-baixa no acelerômetro (referência lenta).
1) Calibrando o bias do giroscópio (do jeito que funciona em bancada)
Antes de integrar, você precisa estimar o bias com o sensor parado alguns segundos. A ideia é bem direta: ler várias amostras, somar, tirar a média, e considerar isso como “zero”. Se o sensor estiver parado, a taxa real deveria ser ~0 °/s; o que sobrar é bias.
#include <stdint.h>
typedef struct {
float gx_bias_dps;
float gy_bias_dps;
float gz_bias_dps;
} mpu6050_gyro_bias_t;
/**
* @brief Estima bias do giroscópio assumindo o sensor parado.
*
* @param read_cb Função que já lê e converte para °/s (use mpu6050_read_raw + convert_to_si).
* @param n Número de amostras para média (ex.: 500 a 2000 dependendo da sua taxa).
*/
bool mpu6050_estimate_gyro_bias(mpu6050_cfg_t *cfg,
mpu6050_gyro_bias_t *bias,
uint16_t n)
{
double sx = 0.0, sy = 0.0, sz = 0.0;
for (uint16_t i = 0; i < n; i++) {
mpu6050_raw_t raw;
mpu6050_si_t si;
if (!mpu6050_read_raw(&raw)) return false;
mpu6050_convert_to_si(cfg, &raw, &si);
sx += si.gx_dps;
sy += si.gy_dps;
sz += si.gz_dps;
// aqui você normalmente espera o período de amostragem (delay ou loop sincronizado)
}
bias->gx_bias_dps = (float)(sx / n);
bias->gy_bias_dps = (float)(sy / n);
bias->gz_bias_dps = (float)(sz / n);
return true;
}
O motivo de fazer isso no começo do firmware é que o bias varia com temperatura, montagem e até com a alimentação. Mesmo que você use o DMP depois, entender e validar o bias com leitura crua te dá controle e diagnósticos melhores quando algo “parece errado”.
2) Integração do giroscópio para obter ângulo (roll/pitch) e por que deriva
Agora sim você integra. Aqui eu vou integrar roll e pitch usando \(g_x\) e \(g_y\) (em °/s), subtraindo o bias. O yaw (rotação em torno de Z) também dá para integrar, mas sem magnetômetro ele deriva sem referência absoluta; no MPU6050 puro, yaw só fica “bom” com fusão avançada e ainda assim deriva com o tempo se você não tiver referência externa.
#include <math.h>
typedef struct {
float roll_deg;
float pitch_deg;
} mpu6050_angles_t;
/**
* @brief Integra giroscópio (com bias removido) para atualizar ângulos.
*
* @param dt_s Intervalo de amostragem em segundos (muito importante medir bem).
*/
void mpu6050_integrate_gyro(const mpu6050_si_t *si,
const mpu6050_gyro_bias_t *bias,
float dt_s,
mpu6050_angles_t *angles_io)
{
const float gx = si->gx_dps - bias->gx_bias_dps;
const float gy = si->gy_dps - bias->gy_bias_dps;
// Integração Euler: theta(t+dt) = theta(t) + omega * dt
angles_io->roll_deg += gx * dt_s;
angles_io->pitch_deg += gy * dt_s;
}
Se você rodar isso “puro”, vai perceber um comportamento muito típico: no começo parece perfeito, mas depois de alguns segundos/minutos o ângulo começa a “andar sozinho”. Isso é a deriva causada por bias residual e por ruído integrado. Por isso a gente corrige com o acelerômetro em baixa frequência.
3) Filtro complementar: giroscópio rápido + acelerômetro estável (pitch/roll)
O filtro complementar pode ser entendido como uma mistura ponderada: você confia mais no giroscópio para o curto prazo (porque ele é suave e responde rápido) e usa o acelerômetro para “puxar” o valor de volta lentamente (porque ele não deriva, mas sofre com aceleração linear e vibração). Uma forma canônica é:
\[
\theta = \alpha(\theta + \omega \Delta t) + (1-\alpha)\theta_{acc}
\]
onde \(\alpha\) geralmente é algo como 0.98 (mas você ajusta conforme sua taxa de amostragem e ruído).
A seguir, eu amarro tudo: calcula inclinação por acelerômetro (a função da seção anterior), integra gyro, e faz o blend.
#include <math.h>
typedef struct {
float alpha; // 0..1 (ex.: 0.98)
} mpu6050_comp_filter_t;
void mpu6050_complementary_update(const mpu6050_si_t *si,
const mpu6050_gyro_bias_t *bias,
float dt_s,
const mpu6050_comp_filter_t *f,
mpu6050_angles_t *angles_io)
{
// 1) Ângulo vindo do acelerômetro (referência lenta)
mpu6050_tilt_t tilt_acc;
mpu6050_tilt_from_accel(si->ax_g, si->ay_g, si->az_g, &tilt_acc);
// 2) Predição pelo giroscópio (movimento rápido)
const float gx = si->gx_dps - bias->gx_bias_dps;
const float gy = si->gy_dps - bias->gy_bias_dps;
const float roll_pred = angles_io->roll_deg + gx * dt_s;
const float pitch_pred = angles_io->pitch_deg + gy * dt_s;
// 3) Combinação (complementar)
angles_io->roll_deg = f->alpha * roll_pred + (1.0f - f->alpha) * tilt_acc.roll_deg;
angles_io->pitch_deg = f->alpha * pitch_pred + (1.0f - f->alpha) * tilt_acc.pitch_deg;
}
O que essa função faz, fisicamente, é “deixar o giroscópio mandar” a curto prazo e impedir que o ângulo se perca a longo prazo. Se você usar o sensor num robô, drone pequeno, gimbal simples ou wearables, esse filtro costuma ser a primeira solução realmente utilizável antes de entrar em quaternions. A trigonometria do acelerômetro (com atan2 e a normalização no pitch) é justamente o que fornece a referência lenta que impede a deriva.
4) Medindo o dt direito (onde muita gente erra)
Se o seu dt_s oscilar muito, a integração vira ruído. O ideal é ter um timer de alta resolução e calcular dt_s como diferença entre timestamps. Em STM32, por exemplo, você normalmente pega um contador de microssegundos; em AVR, pode ser micros(); em ESP32, esp_timer_get_time(). O ponto é: o filtro depende de dt_s confiável.
Aqui vai um esqueleto genérico usando microssegundos, para você adaptar na sua plataforma:
extern uint32_t micros32(void); // você implementa: retorna microssegundos (overflow ok)
static float dt_from_micros(uint32_t *t_prev_us)
{
const uint32_t now = micros32();
const uint32_t dt_us = (uint32_t)(now - *t_prev_us); // overflow-friendly em unsigned
*t_prev_us = now;
return (float)dt_us * 1e-6f;
}
Você chama dt_from_micros() uma vez por loop e usa o retorno como dt_s.