MCU & FPGA geral Quaternions para IMU e filtros Madgwick/Mahony (com exemplos em C)

Quaternions para IMU e filtros Madgwick/Mahony (com exemplos em C)


Quando você pega um giroscópio e integra a velocidade angular ao longo do tempo, você obtém atitude (orientação). O problema é que “representar atitude” com ângulos de Euler (roll/pitch/yaw) parece intuitivo, mas dá dor de cabeça em firmware: aparece gimbal lock (singularidades), as equações ficam cheias de senos/cossenos acoplados e pequenas imprecisões numéricas explodem mais fácil. Quaternions entram justamente para representar rotações 3D de forma estável, contínua e eficiente: você atualiza a orientação com multiplicações e normalizações, e só converte para Euler no final, se precisar mostrar em tela ou enviar telemetria.

Um quaternion unitário pode ser escrito como (q = [q_w,; q_x,; q_y,; q_z]), onde (q_w) é a parte escalar e \([q_x,q_y,q_z]\) é a parte vetorial. Para atitude, usamos quaternion unitário, isto é, \(||q||=1\). Isso é crucial: se o módulo “anda”, sua rotação deixa de ser pura e você começa a introduzir distorção numérica. Por isso, em sistemas embarcados, normalizar o quaternion frequentemente não é luxo; é parte do controle de erro.

A ideia central: derivada do quaternion a partir do giroscópio

Se o giroscópio mede \(\boldsymbol{\omega} = [\omega_x,\omega_y,\omega_z]\) em rad/s no referencial do corpo, a cinemática do quaternion é:

\[
\dot{q} = \frac{1}{2}; q \otimes \Omega
\quad \text{onde}\quad
\Omega = [0,;\omega_x,;\omega_y,;\omega_z]
\]

e \(\otimes\) é o produto de quaternions. Discretizando com passo (dt):

\[
q_{k+1} \approx q_k + \dot{q}; dt
\]
e depois você normaliza \(q_{k+1}\).

Isso sozinho já dá um “filtro”: é a integração do giro. Só que o giroscópio tem bias (deslocamento DC) e ruído. A longo prazo, o erro cresce. É aí que entram acelerômetro e magnetômetro como “âncoras” para corrigir drift.

Como o acelerômetro ajuda sem “medir rotação”

O acelerômetro mede “aceleração específica”, que em baixa dinâmica (ou com filtragem) aproxima a direção da gravidade. Ele não te dá yaw (guinada) absoluto, mas dá um ótimo pitch/roll de referência. Já o magnetômetro dá yaw absoluto (com todos os cuidados de calibração/soft-iron/hard-iron).

Madgwick e Mahony fazem a mesma coisa em espírito: usam o erro entre o que o sensor “deveria ver” pela orientação atual e o que ele realmente vê, e retroalimentam isso no giro. A diferença é o jeito matemático dessa retroalimentação.


Madgwick: correção por gradiente (rápida e boa para MCU)

No Madgwick, você ajusta o quaternion para minimizar uma função de erro baseada na diferença entre o vetor gravidade previsto e o gravidade medido (e opcionalmente o campo magnético). A implementação clássica aplica um termo de “descida do gradiente” (\nabla f) e mistura isso na atualização:

\[
\dot{q} = \frac{1}{2}; q \otimes \Omega ;-; \beta ; \frac{\nabla f}{||\nabla f||}
\]

O ganho (\beta) controla quanta “força” você dá para o acelerômetro/magnetômetro corrigirem o drift do giro. Em prática embarcada: (\beta) maior corrige mais rápido, mas pode injetar ruído quando o acelerômetro está sob aceleração linear (robôs, drones, motores vibrando).

Abaixo vai um exemplo IMU-only (gyro + accel), que é a base mais usada em projetos de equilíbrio e estimativa de inclinação. Está em C “puro”, pensado para rodar em Cortex-M/RISC-V sem dependências. (A versão com magnetômetro é uma extensão direta, mas mais longa por causa do modelo do campo magnético.)

#include <math.h>
#include <stdint.h>

typedef struct {
    float w, x, y, z;
} quat_t;

static inline float inv_sqrt(float x) {
    return 1.0f / sqrtf(x);
}

static inline void quat_normalize(quat_t *q) {
    float n = q->w*q->w + q->x*q->x + q->y*q->y + q->z*q->z;
    if (n > 0.0f) {
        float inv = inv_sqrt(n);
        q->w *= inv; q->x *= inv; q->y *= inv; q->z *= inv;
    }
}

static inline quat_t quat_mul(quat_t a, quat_t b) {
    quat_t r;
    r.w = a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z;
    r.x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y;
    r.y = a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x;
    r.z = a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w;
    return r;
}

/**
 * Madgwick IMU update (gyro [rad/s], accel [g] ou m/s^2 normalizado).
 * q: orientação do corpo em relação ao referencial inercial (convenção típica).
 * beta: ganho da correção (comece em algo como 0.05..0.2 dependendo do ruído/dinâmica).
 * dt: período de amostragem em segundos.
 */
void madgwick_update_imu(quat_t *q,
                         float gx, float gy, float gz,
                         float ax, float ay, float az,
                         float beta, float dt)
{
    // Se accel for zero (sensor saturado/desligado), integra apenas giro
    float a2 = ax*ax + ay*ay + az*az;
    if (a2 <= 1e-12f) {
        quat_t omega = {0.0f, gx, gy, gz};
        quat_t qdot = quat_mul(*q, omega);
        qdot.w *= 0.5f; qdot.x *= 0.5f; qdot.y *= 0.5f; qdot.z *= 0.5f;

        q->w += qdot.w * dt; q->x += qdot.x * dt;
        q->y += qdot.y * dt; q->z += qdot.z * dt;
        quat_normalize(q);
        return;
    }

    // Normaliza aceleração (direção da gravidade medida)
    float inva = inv_sqrt(a2);
    ax *= inva; ay *= inva; az *= inva;

    // Atalhos
    float q1 = q->w, q2 = q->x, q3 = q->y, q4 = q->z;

    // Função de erro (IMU): gravidade prevista vs medida
    // A gravidade prevista (no corpo) pode ser derivada do quaternion.
    // Aqui usamos a forma popular do algoritmo (gradiente já “embutido”).
    float _2q1 = 2.0f*q1;
    float _2q2 = 2.0f*q2;
    float _2q3 = 2.0f*q3;
    float _2q4 = 2.0f*q4;

    float _4q1 = 4.0f*q1;
    float _4q2 = 4.0f*q2;
    float _4q3 = 4.0f*q3;

    float _8q2 = 8.0f*q2;
    float _8q3 = 8.0f*q3;

    float q1q1 = q1*q1;
    float q2q2 = q2*q2;
    float q3q3 = q3*q3;
    float q4q4 = q4*q4;

    // Gradiente (s1..s4)
    float s1 = _4q1*q3q3 + _2q3*ax + _4q1*q2q2 - _2q2*ay;
    float s2 = _4q2*q4q4 - _2q4*ax + 4.0f*q1q1*q2 - _2q1*ay - _4q2 + _8q2*q2q2 + _8q2*q3q3 + _4q2*az;
    float s3 = 4.0f*q1q1*q3 + _2q1*ax + _4q3*q4q4 - _2q4*ay - _4q3 + _8q3*q2q2 + _8q3*q3q3 + _4q3*az;
    float s4 = 4.0f*q2q2*q4 - _2q2*ax + 4.0f*q3q3*q4 - _2q3*ay;

    // Normaliza o gradiente para estabilidade numérica
    float s2n = s1*s1 + s2*s2 + s3*s3 + s4*s4;
    if (s2n > 1e-12f) {
        float invs = inv_sqrt(s2n);
        s1 *= invs; s2 *= invs; s3 *= invs; s4 *= invs;
    }

    // qdot = 0.5*q⊗omega - beta*s
    quat_t omega = {0.0f, gx, gy, gz};
    quat_t qdot = quat_mul(*q, omega);
    qdot.w *= 0.5f; qdot.x *= 0.5f; qdot.y *= 0.5f; qdot.z *= 0.5f;

    qdot.w -= beta * s1;
    qdot.x -= beta * s2;
    qdot.y -= beta * s3;
    qdot.z -= beta * s4;

    // Integra
    q->w += qdot.w * dt; q->x += qdot.x * dt;
    q->y += qdot.y * dt; q->z += qdot.z * dt;

    // Mantém unitário
    quat_normalize(q);
}

Esse núcleo já resolve a parte mais importante: você sai de uma IMU crua para uma orientação estável, com drift bem menor que integração pura do giro. Em produto real, o que mais derruba qualidade não é “o algoritmo em si”, e sim: unidade errada (graus/s em vez de rad/s), (dt) inconsistente, aceleração linear contaminando o acelerômetro, e falta de calibração do bias do giro.


Mahony: realimentação PI no erro (controle clássico e intuitivo)

O Mahony parece, conceitualmente, um controle de atitude: você calcula um vetor de erro (\mathbf{e}) como produto vetorial entre a gravidade medida e a gravidade prevista, e usa isso para corrigir o giroscópio. A forma típica faz:

\[
\boldsymbol{\omega}_{corr} = \boldsymbol{\omega} + K_p \mathbf{e} + K_i \int \mathbf{e},dt
\]

Depois você integra o quaternion usando (\boldsymbol{\omega}_{corr}). A sacada é que o termo integral atua como estimativa do bias do giroscópio. Em firmware, isso costuma ficar muito robusto quando o dispositivo tem períodos frequentes de “quase-estático” (por exemplo, robô que para, equipamento fixo, wearable em repouso).

Exemplo IMU-only em C:

#include <math.h>
#include <stdint.h>

typedef struct { float w, x, y, z; } quat_t;

static inline float inv_sqrt(float x) { return 1.0f / sqrtf(x); }

static inline void quat_normalize(quat_t *q) {
    float n = q->w*q->w + q->x*q->x + q->y*q->y + q->z*q->z;
    if (n > 0.0f) {
        float inv = inv_sqrt(n);
        q->w *= inv; q->x *= inv; q->y *= inv; q->z *= inv;
    }
}

static inline quat_t quat_mul(quat_t a, quat_t b) {
    quat_t r;
    r.w = a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z;
    r.x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y;
    r.y = a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x;
    r.z = a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w;
    return r;
}

/**
 * Mahony IMU update.
 * kp: ganho proporcional (correção rápida).
 * ki: ganho integral (estimativa de bias).
 * integral_fb: estado do integrador (persistente).
 */
void mahony_update_imu(quat_t *q,
                       float gx, float gy, float gz,
                       float ax, float ay, float az,
                       float kp, float ki, float dt,
                       float integral_fb[3])
{
    float a2 = ax*ax + ay*ay + az*az;
    if (a2 <= 1e-12f) {
        // Sem accel válido: integra giro sem correção
        quat_t omega = {0.0f, gx, gy, gz};
        quat_t qdot = quat_mul(*q, omega);
        qdot.w *= 0.5f; qdot.x *= 0.5f; qdot.y *= 0.5f; qdot.z *= 0.5f;

        q->w += qdot.w * dt; q->x += qdot.x * dt;
        q->y += qdot.y * dt; q->z += qdot.z * dt;
        quat_normalize(q);
        return;
    }

    // Normaliza accel (direção gravidade medida)
    float inva = inv_sqrt(a2);
    ax *= inva; ay *= inva; az *= inva;

    // Gravidade prevista no corpo a partir do quaternion (convenção comum)
    // v = [2(qx qz - qw qy), 2(qw qx + qy qz), qw^2 - qx^2 - qy^2 + qz^2]
    float qw = q->w, qx = q->x, qy = q->y, qz = q->z;

    float vx = 2.0f*(qx*qz - qw*qy);
    float vy = 2.0f*(qw*qx + qy*qz);
    float vz = qw*qw - qx*qx - qy*qy + qz*qz;

    // Erro = (gravidade medida) x (gravidade prevista)
    // e = a × v
    float ex = (ay*vz - az*vy);
    float ey = (az*vx - ax*vz);
    float ez = (ax*vy - ay*vx);

    // Integra erro para estimar bias (termo I)
    if (ki > 0.0f) {
        integral_fb[0] += ki * ex * dt;
        integral_fb[1] += ki * ey * dt;
        integral_fb[2] += ki * ez * dt;
    } else {
        integral_fb[0] = integral_fb[1] = integral_fb[2] = 0.0f;
    }

    // Corrige giro
    gx += kp * ex + integral_fb[0];
    gy += kp * ey + integral_fb[1];
    gz += kp * ez + integral_fb[2];

    // Integra quaternion com giro corrigido
    quat_t omega = {0.0f, gx, gy, gz};
    quat_t qdot = quat_mul(*q, omega);
    qdot.w *= 0.5f; qdot.x *= 0.5f; qdot.y *= 0.5f; qdot.z *= 0.5f;

    q->w += qdot.w * dt; q->x += qdot.x * dt;
    q->y += qdot.y * dt; q->z += qdot.z * dt;
    quat_normalize(q);
}

Na prática, você escolhe (K_p) para “colar” o pitch/roll rapidamente no acelerômetro quando estiver estável, e (K_i) pequeno para não explodir em dinâmica. Um detalhe de firmware que faz diferença: se o seu sistema passa longos períodos sob aceleração linear (por exemplo, um robô andando), vale reduzir (K_p) adaptativamente quando (|\mathbf{a}|) se afasta muito de 1g, porque isso indica que o acelerômetro não está “vendo só gravidade”.


Convertendo quaternion para Euler (só para exibir / log)

Mesmo que você use quaternion internamente, muitas interfaces querem Euler. A conversão típica (uma convenção comum) é:

#include <math.h>

void quat_to_euler_rad(const quat_t *q, float *roll, float *pitch, float *yaw)
{
    float qw = q->w, qx = q->x, qy = q->y, qz = q->z;

    // roll (x-axis rotation)
    float sinr_cosp = 2.0f * (qw*qx + qy*qz);
    float cosr_cosp = 1.0f - 2.0f * (qx*qx + qy*qy);
    *roll = atan2f(sinr_cosp, cosr_cosp);

    // pitch (y-axis rotation)
    float sinp = 2.0f * (qw*qy - qz*qx);
    if (fabsf(sinp) >= 1.0f) {
        *pitch = copysignf((float)M_PI / 2.0f, sinp); // saturação
    } else {
        *pitch = asinf(sinp);
    }

    // yaw (z-axis rotation)
    float siny_cosp = 2.0f * (qw*qz + qx*qy);
    float cosy_cosp = 1.0f - 2.0f * (qy*qy + qz*qz);
    *yaw = atan2f(siny_cosp, cosy_cosp);
}

Note que Euler pode ter singularidade em pitch perto de ±90°, mas isso não “estraga” o filtro se você só usa Euler como saída final.


Madgwick vs Mahony: quando escolher qual (na vida real)

Se você quer um filtro muito direto, rápido, com comportamento excelente em MCU e fácil de portar, Madgwick é normalmente uma escolha “plugável”, principalmente na versão IMU-only. Mahony costuma brilhar quando você quer uma interpretação mais “controle PI”, com ajuste intuitivo de (K_p/K_i) e boa correção de bias em cenários com trechos estáveis. Em ambos, a diferença entre “funciona” e “fica profissional” quase sempre está menos no algoritmo e mais na engenharia: calibração do giroscópio, sincronização de amostragem, filtragem do acelerômetro, e consistência de (dt).

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

Related Post

0
Adoraria saber sua opinião, comente.x