6. Calibração, Salvamento de Offsets e Uso de Aceleração Linear para Estimar Movimento
O BNO055 só entrega orientação realmente confiável quando o algoritmo interno está bem calibrado. Isso não é “perfumaria”: como o giroscópio deriva com o tempo, como o magnetômetro sofre com interferência (hard/soft iron) e como o acelerômetro mede gravidade misturada com aceleração linear, a fusão depende de offsets e correções internas. A Bosch expõe isso de forma objetiva via o registrador CALIB_STAT, e a Adafruit mostra esse status na prática em seus exemplos e guias. (cdn-learn.adafruit.com)
6.1 Lendo o status de calibração (CALIB_STAT)
O registrador CALIB_STAT (0x35) codifica quatro níveis (0 a 3) para: sistema (fusão), giroscópio, acelerômetro e magnetômetro. A leitura que você já viu na seção anterior é exatamente o caminho correto, e é a forma mais segura de decidir quando “confiar” nos dados para navegação/controle fechado. No uso real, o que costuma acontecer é: gyro e accel chegam ao nível 3 rapidamente, enquanto o mag pode ficar em 1–2 se houver perturbação magnética no ambiente.
6.2 Por que salvar calibração vale ouro em produto real
Em um protótipo, recalibrar “na mão” pode ser aceitável. Em produto, é ruim: o usuário liga e já quer funcionar. Por isso, a estratégia comum é: depois de atingir calibração adequada, você lê os offsets/calibração do BNO055 e salva em flash/EEPROM do microcontrolador hospedeiro. No próximo boot, você restaura. Esse fluxo é praticado pela comunidade e é compatível com o modelo de “sensor hub” do BNO055 (que mantém informações de calibração internamente e as expõe por registradores). (Bosch Sensortec)
O ponto crítico aqui é respeitar o modo CONFIGMODE antes de escrever offsets. Se você tentar gravar offsets em modo NDOF, a chance de comportamento estranho aumenta, porque o firmware interno está ativamente atualizando estados.
6.3 Rotina em C para ler e restaurar offsets
A Bosch define um bloco de registradores de offsets e raios (principalmente para mag) e offsets para accel/gyro. Em um driver robusto, você encapsula isso em “read calibration blob” e “write calibration blob”. Um esqueleto seguro (mantendo a lógica de página e modo) fica assim:
#define BNO055_OPR_MODE 0x3D
#define BNO055_PAGE_ID 0x07
#define BNO055_CALIB_STAT 0x35
#define BNO055_SYS_TRIGGER 0x3F
// Offsets (Page 0) - consulte o datasheet para endereços exatos do bloco.
#define BNO055_ACC_OFFSET_X_LSB 0x55 // exemplo de início do bloco
#define BNO055_CALIB_BLOB_LEN 22 // tamanho típico do conjunto (offsets + radius)
/**
* @brief Coloca em CONFIGMODE, aguardando o tempo seguro.
*/
static int BNO055_EnterConfigMode(void)
{
if (BNO055_WriteReg(BNO055_OPR_MODE, 0x00) != 0) return -1;
delay_ms(25);
return 0;
}
/**
* @brief Restaura o modo NDOF, aguardando o tempo seguro.
*/
static int BNO055_EnterNDOF(void)
{
if (BNO055_WriteReg(BNO055_OPR_MODE, 0x0C) != 0) return -1;
delay_ms(25);
return 0;
}
/**
* @brief Lê o "blob" de calibração para salvar em flash do MCU.
*/
int BNO055_ReadCalibrationBlob(uint8_t *blob, uint16_t len)
{
if (!blob || len < BNO055_CALIB_BLOB_LEN) return -1;
// Garantir Page 0
if (BNO055_WriteReg(BNO055_PAGE_ID, 0x00) != 0) return -2;
// Entrar em config para leitura estável de offsets (prática segura)
if (BNO055_EnterConfigMode() != 0) return -3;
int rc = BNO055_ReadReg(BNO055_ACC_OFFSET_X_LSB, blob, BNO055_CALIB_BLOB_LEN);
// Voltar ao modo de fusão
(void)BNO055_EnterNDOF();
return (rc == 0) ? 0 : -4;
}
/**
* @brief Escreve o "blob" de calibração previamente salvo em flash do MCU.
*/
int BNO055_WriteCalibrationBlob(const uint8_t *blob, uint16_t len)
{
if (!blob || len < BNO055_CALIB_BLOB_LEN) return -1;
// Garantir Page 0
if (BNO055_WriteReg(BNO055_PAGE_ID, 0x00) != 0) return -2;
if (BNO055_EnterConfigMode() != 0) return -3;
// Escrita sequencial: reg + bytes (depende da sua HAL)
// Aqui fazemos byte a byte para ficar genérico e tolerante.
for (uint16_t i = 0; i < BNO055_CALIB_BLOB_LEN; i++)
{
if (BNO055_WriteReg((uint8_t)(BNO055_ACC_OFFSET_X_LSB + i), blob[i]) != 0)
{
(void)BNO055_EnterNDOF();
return -4;
}
}
return (BNO055_EnterNDOF() == 0) ? 0 : -5;
}
Repare que eu deixei explícito que o endereço de início do bloco e o tamanho devem ser conferidos no datasheet. Esse cuidado é essencial para não “inventar” offsets que não existam, porque revisões do datasheet e libs podem tratar blocos de forma ligeiramente diferente. (Bosch Sensortec)
6.4 Aceleração linear e “movimento”: o que dá para fazer e o que não dá
O BNO055 fornece um vetor chamado Linear Acceleration, que é (em tese) a aceleração do corpo com a gravidade removida usando a orientação estimada. Isso é extremamente útil para identificar eventos de movimento (início/fim de deslocamento, vibração, impacto, passos), mas é perigoso para “posição absoluta”, porque integrar aceleração duas vezes explode o erro muito rápido:
\[
v(t) = \int a(t),dt,\quad x(t)=\int v(t),dt
\]
Qualquer offset pequeno vira uma deriva grande em poucos segundos. Em robótica, dá para usar isso em janelas curtas com técnicas como ZUPT (Zero Velocity Update) quando você detecta que o sistema está parado, mas o BNO055 sozinho não faz milagres. A Adafruit trata o BNO055 como excelente para orientação e bom para vetores derivados (gravidade/linear accel), porém não como solução de navegação inercial completa. (cdn-learn.adafruit.com)
Um exemplo prático e seguro é usar aceleração linear para detectar deslocamento e estimar velocidade apenas em janelas pequenas, travando a velocidade em zero quando a norma da aceleração fica abaixo de um limiar por um tempo:
typedef struct
{
float vx, vy, vz;
float x, y, z;
} motion_state_t;
/**
* @brief Integra aceleração para obter velocidade e posição (uso apenas em janelas curtas).
* @note Isso deriva rápido. Use para detecção/estimativa local e aplique ZUPT.
*/
void Motion_Update(motion_state_t *s, float ax, float ay, float az, float dt_s)
{
// Integra aceleração -> velocidade
s->vx += ax * dt_s;
s->vy += ay * dt_s;
s->vz += az * dt_s;
// Integra velocidade -> posição
s->x += s->vx * dt_s;
s->y += s->vy * dt_s;
s->z += s->vz * dt_s;
}
/**
* @brief ZUPT simples: se a aceleração linear estiver "quase zero", zera velocidade.
*/
void Motion_ApplyZUPT(motion_state_t *s, float ax, float ay, float az)
{
float norm = sqrtf(ax*ax + ay*ay + az*az);
// limiar depende do ruído e escala (ex.: 0.05 m/s^2 é um palpite típico, calibra em bancada)
if (norm < 0.05f)
{
s->vx = 0.0f;
s->vy = 0.0f;
s->vz = 0.0f;
}
}
O ponto didático importante: “linear acceleration” do BNO055 já ajuda bastante, porque você evita ter que subtrair gravidade manualmente usando quaternions. Ainda assim, qualquer integração precisa de estratégia anti-deriva.
6.5 Ligando isso ao problema do I2C em produto real
Rotinas de calibração e leitura de vetores são justamente momentos em que o firmware tende a fazer leituras longas e sequenciais. Se você estiver num host com I2C problemático (como Raspberry Pi com o conhecido problema de clock stretching no hardware I2C), isso vira travamento frequente. Por isso, a própria Adafruit recomenda workaround como reduzir clock ou migrar para UART em Pi quando necessário. (cdn-learn.adafruit.com)
Na próxima seção, eu fecho o elo: vou consolidar, com base em fontes públicas da Adafruit e relatos técnicos de ecossistemas (ESP8266/ESP32/Raspberry Pi), quais famílias de microcontroladores/hosts dão mais dor de cabeça, por que (clock stretching, timeouts, drivers), e boas práticas de engenharia (pull-ups, velocidade, reset de barramento, retries, separação em tarefa/RTOS).