PTENES
MÓDULO 2.2 · FUNDAMENTO

🧭 IMU e encoders: saber onde está

Um robô autônomo precisa responder a duas perguntas sem ajuda de ninguém: "para onde estou virado?" e "quanto andei?". A IMU responde a primeira; os encoders das rodas, a segunda. Juntos dão a odometria, a posição estimada do robô, com erros que você vai aprender a medir e reduzir.

6
Tópicos
Intermediário
Nível
Fundamento
Tipo
IMU + encoders
Hardware
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

🧭 O que uma IMU mede (acelerômetro e giroscópio)

A IMU (unidade de medida inercial) é um chip que sente movimento sem olhar para fora. A MPU-6050 tem dois sensores de três eixos cada. O acelerômetro mede aceleração, incluindo a gravidade: parado e reto, ele mostra 1 g apontando para baixo, e por isso serve para saber a inclinação. O giroscópio mede a velocidade de giro em cada eixo, em graus por segundo; somando essa velocidade ao longo do tempo, você sabe quanto o robô virou.

🌱 Novo aqui?

g é a aceleração da gravidade (cerca de 9,8 m/s²); 1 g é o que você sente parado. °/s (graus por segundo) é velocidade de giro: 90 °/s é um quarto de volta por segundo. Integrar é somar uma velocidade, pedacinho por pedacinho de tempo, para obter o total (ângulo ou distância). Deriva (drift) é o erro pequeno que, somado muitas vezes, cresce sem parar.

IMU X frenteZ cimaY esquerdagiro em Z = virargravidade (1 g) acelerômetroinclinação: bomcom tranco: ruidoso giroscópiogiro rápido: ótimocom o tempo: deriva

Como ler: as setas azuis são os três eixos, sempre presos ao chip. A seta âmbar é a gravidade que o acelerômetro sente; a curva ciano é o giro em torno de Z, que o giroscópio mede quando o robô vira. Os dois quadros resumem o ponto forte e o fraco de cada sensor: um completa o outro, e é isso que o tópico 3 usa.

IMUO que temQuando escolher
MPU-6050acelerômetro + giroscópio (6 eixos), I2Cbarata, muito documentada, ótima para aprender
ICM-209486 eixos + magnetômetro (9 eixos), I2C/SPIquando precisar de bússola; use a biblioteca do fabricante da placa

Por que aprender

Girar exatamente 90°, perceber que subiu numa rampa ou detectar que tombou são tarefas da IMU. Sem ela, o robô só sabe quanto as rodas giraram, e roda escorrega.

Conceitos-chave

Acelerômetro
aceleração + gravidade
Giroscópio
velocidade de giro
6 / 9 eixos
com ou sem bússola
Deriva
erro que acumula
2

🔌 Ler a MPU-6050 por I2C

A MPU-6050 fala I2C, o barramento de dois fios que você conheceu no 1.4. São quatro ligações: VCC no 3V3, GND no GND, SDA no GPIO 21 e SCL no GPIO 22. Em vez de uma biblioteca pronta, você vai conversar direto com o chip: escrever num registrador para acordá-lo e ler 14 bytes de uma vez, com aceleração, temperatura e giro.

🌱 Novo aqui?

Um registrador é uma posição de memória dentro do sensor, com um número (ex.: 0x6B). Escrever nele muda a configuração; ler dele traz a medida. O 0x indica número em hexadecimal. Cada medida vem em 2 bytes, um número inteiro de −32768 a 32767; dividir por 16384 dá a aceleração em g, e por 131, o giro em °/s (faixas padrão de ±2 g e ±250 °/s).

ESP32MPU-6050 3V3GNDGPIO 21GPIO 22 VCCGNDSDASCL a partir de 0x3Bacel. X, Y, Ztemperaturagiro X, Y, Z14 bytes2 por medida módulo GY-521: AD0 no GND = endereço 0x68

Como ler: à esquerda, os quatro fios; vermelho é alimentação, cinza é terra, azul e ciano são os dados do I2C. À direita, a ordem em que os 14 bytes chegam quando você pede a leitura a partir do registrador 0x3B. O código lê nessa mesma ordem.

Objetivo: acordar a MPU-6050 e mostrar aceleração (g) e giro (°/s) dez vezes por segundo. Funciona no Wokwi, que tem a MPU-6050 na lista de peças.

mpu6050-leitura.ino
// MPU-6050 por I2C, sem biblioteca extra (pacote esp32 3.x)
#include <Wire.h>
const int MPU = 0x68;                     // endereço com AD0 no GND

int16_t ler16() {                          // junta dois bytes: alto primeiro
  uint8_t alto = Wire.read();
  uint8_t baixo = Wire.read();
  return (int16_t)((alto << 8) | baixo);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);                      // SDA, SCL
  Wire.beginTransmission(MPU);
  Wire.write(0x6B); Wire.write(0);         // registrador de energia = 0: sai do modo sono
  if (Wire.endTransmission() != 0) {
    Serial.println("MPU-6050 nao respondeu: confira fios e endereco");
    while (true) delay(1000);
  }
  Serial.println("MPU-6050 pronta");
}

void loop() {
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);                        // começa na aceleração X
  Wire.endTransmission(false);
  Wire.requestFrom(MPU, 14);
  float ax = ler16() / 16384.0;
  float ay = ler16() / 16384.0;
  float az = ler16() / 16384.0;
  ler16();                                 // temperatura: ignorada aqui
  float gx = ler16() / 131.0;
  float gy = ler16() / 131.0;
  float gz = ler16() / 131.0;
  Serial.printf("ax:%.2f,ay:%.2f,az:%.2f,gx:%.1f,gy:%.1f,gz:%.1f\n", ax, ay, az, gx, gy, gz);
  delay(100);
}

Como verificar

  • ✓Placa parada e deitada: az perto de 1.00, ax e ay perto de 0, giros perto de 0 (até ±3 é normal).
  • ✓Gire a placa sobre a mesa: gz sobe enquanto ela gira e volta a perto de 0 quando para.
  • ✗"nao respondeu"? Rode o scanner I2C do 1.4: se aparecer 0x69, o pino AD0 está em 3V3.

✓ Montagem que funciona

  • ✓IMU presa firme no chassi, perto do centro, com os eixos alinhados ao robô.
  • ✓Fios curtos de I2C, longe dos fios do motor.
  • ✓Robô parado nos primeiros segundos para calibrar o giro.

✗ Fonte de leitura maluca

  • ✗IMU solta na protoboard, balançando a cada tranco.
  • ✗IMU colada no motor: a vibração vira ruído.
  • ✗Alimentar com 5 V um módulo sem regulador.

Por que aprender

Ler registradores direto mostra o que as bibliotecas escondem e funciona igual para quase todo sensor I2C: endereço, registrador, quantidade de bytes, conversão de unidade. Quando trocar de IMU, você saberá onde procurar na folha de dados.

Conceitos-chave

Registrador
endereço interno
0x68
endereço I2C
16384 / 131
bruto → g e °/s
Leitura em rajada
14 bytes de uma vez
3

📐 Ângulo com filtro complementar

Dá para calcular a inclinação de dois jeitos. Pelo acelerômetro, com trigonometria sobre a gravidade: certo na média, mas pula a cada tranco. Pelo giroscópio, somando o giro: liso e rápido, mas a deriva empurra o resultado para longe. O filtro complementar mistura os dois: confia 98% no giroscópio a cada passo e usa 2% do acelerômetro para puxar o ângulo de volta à verdade.

giroscópio°/s, rápido acelerômetroatan2, ruidoso ângulo + giro×dt× 0,98 ângulo da gravidade× 0,02+ ângulo filtradoo resultado de agora é o ponto de partida do próximo passo

Como ler: o caminho de cima (ciano) segue o movimento rápido; o de baixo (azul) corrige devagar a deriva. Os pesos somam 1. A seta tracejada mostra que o filtro tem memória: cada novo ângulo parte do anterior. Mais peso no giroscópio deixa o ângulo mais liso; mais peso no acelerômetro corrige mais rápido, com mais ruído.

Objetivo: calcular a inclinação para a frente (arfagem) de três jeitos e ver os três no Serial Plotter: só acelerômetro, só giroscópio e filtrado. O laço roda em tempo fixo de 10 ms.

filtro-complementar.ino
// Inclinação com filtro complementar (MPU-6050, pacote esp32 3.x)
#include <Wire.h>
const int MPU = 0x68;
const float ALFA = 0.98;                  // confiança no giroscópio
const float DT = 0.01;                    // 10 ms por passo
float offsetGy = 0, angGiro = 0, angFiltro = 0;

int16_t ler16() { uint8_t a = Wire.read(); uint8_t b = Wire.read(); return (int16_t)((a << 8) | b); }

void lerMPU(float &ax, float &ay, float &az, float &gy) {
  Wire.beginTransmission(MPU); Wire.write(0x3B); Wire.endTransmission(false);
  Wire.requestFrom(MPU, 14);
  ax = ler16() / 16384.0; ay = ler16() / 16384.0; az = ler16() / 16384.0;
  ler16(); ler16();                        // temperatura e giro X: ignorados
  gy = ler16() / 131.0; ler16();           // giro Y (usado) e giro Z
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  Wire.beginTransmission(MPU); Wire.write(0x6B); Wire.write(0); Wire.endTransmission();
  Serial.println("Calibrando: nao mexa no robo...");
  float ax, ay, az, gy, soma = 0;
  for (int i = 0; i < 500; i++) { lerMPU(ax, ay, az, gy); soma += gy; delay(2); }
  offsetGy = soma / 500;                   // o giro "parado" vira o zero
  lerMPU(ax, ay, az, gy);
  angFiltro = angGiro = atan2(-ax, sqrt(ay * ay + az * az)) * 180 / PI;
}

void loop() {
  static unsigned long ultimo = micros();
  if (micros() - ultimo < 10000) return;   // espera completar 10 ms
  ultimo += 10000;
  float ax, ay, az, gy;
  lerMPU(ax, ay, az, gy);
  gy -= offsetGy;
  float angAcel = atan2(-ax, sqrt(ay * ay + az * az)) * 180 / PI;
  angGiro += gy * DT;                                          // só giroscópio: deriva
  angFiltro = ALFA * (angFiltro + gy * DT) + (1 - ALFA) * angAcel;
  Serial.printf("acel:%.1f,giro:%.1f,filtro:%.1f\n", angAcel, angGiro, angFiltro);
}

Como verificar

  • ✓Abra Ferramentas → Serial Plotter. Incline a placa uns 30°: as três linhas sobem juntas.
  • ✓Bata de leve na mesa: "acel" dá picos, "filtro" quase não mexe.
  • ✓Deixe parado alguns minutos: "giro" se afasta devagar do valor real; "filtro" não.

Por que aprender

Cinco linhas de conta resolvem o problema que derruba robôs equilibristas e drones caseiros. O filtro de Kalman, que você vai encontrar em projetos maiores, segue a mesma ideia de juntar um sensor rápido com um sensor que não deriva, com pesos que se ajustam sozinhos.

Conceitos-chave

atan2
ângulo pela gravidade
Offset
zero do giroscópio
α = 0,98
peso do giroscópio
dt fixo
laço de 10 ms
4

⚙️ Encoders: contar giros da roda

Um encoder transforma giro em pulsos. No motor TT, um disco com 20 furos passa entre um LED e um sensor de luz: cada furo é um pulso. No motor N20 com encoder, um ímã no eixo passa por dois sensores Hall. Os dois sensores, chamados canais A e B, ficam defasados: quem pulsa primeiro diz o sentido do giro. Isso se chama quadratura.

🌱 Novo aqui?

Uma interrupção é um aviso do hardware que pausa o programa por um instante para rodar uma função curta, a ISR, e depois continua de onde parou. Assim nenhum pulso se perde, mesmo com o loop ocupado. Variáveis mudadas dentro da ISR levam volatile, e a ISR leva IRAM_ATTR para rodar da memória rápida do ESP32.

AB A sobe antes de Bfrente: A na frente de B · ré: B na frente de A

Como ler: as duas ondas têm o mesmo ritmo, mas B está atrasada um quarto de ciclo. Nas linhas âmbar, A subiu primeiro: a roda gira para a frente. Se a roda inverter, B passa a mudar primeiro. Contar cada borda de A (subida e descida) dobra a resolução sem custo.

Objetivo: contar os pulsos das duas rodas com interrupção e mostrar a rotação em RPM a cada 200 ms. Encoders: esquerdo A 18 / B 19, direito A 34 / B 35 (34 e 35 não têm pull-up interno; os módulos Hall do N20 costumam ter).

encoders-rpm.ino
// Dois encoders em quadratura com interrupção (pacote esp32 3.x)
const int ENC_E_A = 18, ENC_E_B = 19;      // roda esquerda
const int ENC_D_A = 34, ENC_D_B = 35;      // roda direita
const float PULSOS_VOLTA = 1400;          // N20 1:100 contando as 2 bordas de A. Meça o seu (tópico 6)
volatile long pulsosE = 0, pulsosD = 0;

void IRAM_ATTR isrE() { if (digitalRead(ENC_E_A) == digitalRead(ENC_E_B)) pulsosE++; else pulsosE--; }
void IRAM_ATTR isrD() { if (digitalRead(ENC_D_A) == digitalRead(ENC_D_B)) pulsosD--; else pulsosD++; }  // espelhado

void setup() {
  Serial.begin(115200);
  pinMode(ENC_E_A, INPUT_PULLUP); pinMode(ENC_E_B, INPUT_PULLUP);
  pinMode(ENC_D_A, INPUT); pinMode(ENC_D_B, INPUT);
  attachInterrupt(digitalPinToInterrupt(ENC_E_A), isrE, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_D_A), isrD, CHANGE);
  Serial.println("Encoders prontos: gire as rodas com a mao");
}

void loop() {
  static long antE = 0, antD = 0;
  delay(200);
  noInterrupts(); long e = pulsosE, d = pulsosD; interrupts();   // cópia sem ser interrompido
  float rpmE = (e - antE) / PULSOS_VOLTA / 0.2 * 60;
  float rpmD = (d - antD) / PULSOS_VOLTA / 0.2 * 60;
  antE = e; antD = d;
  Serial.printf("pulsosE:%ld,pulsosD:%ld,rpmE:%.1f,rpmD:%.1f\n", e, d, rpmE, rpmD);
}

Como verificar

  • ✓Girando a roda esquerda para a frente com a mão, pulsosE sobe; para trás, desce.
  • ✓Rodas paradas: rpm em 0 e contadores parados.
  • ✗Um lado conta ao contrário? Troque o ++ e o −− da ISR daquele lado. Conta e não muda o sinal? O canal B não está ligado.

Por que aprender

Sem encoder, "andar 1 metro" vira "ligar o motor por 2 segundos e torcer". Com ele, você mede velocidade real (a entrada do PID do 2.3) e distância percorrida (a base da odometria do próximo tópico).

Conceitos-chave

Pulsos por volta
resolução da roda
Quadratura
A e B dão o sentido
ISR
curta, IRAM_ATTR
volatile
muda por interrupção
5

🗺️ Odometria: de pulsos a posição (x, y, θ)

Odometria é estimar a posição somando pequenos deslocamentos. A cada 50 ms você pega quanto cada roda andou (pulsos ÷ pulsos por volta × perímetro da roda). A média das duas é quanto o centro do robô avançou; a diferença entre elas, dividida pela distância entre as rodas, é quanto ele girou. Com isso você atualiza x, y e θ (a direção, em radianos).

xy θL a cada passod = (dE + dD) / 2Δθ = (dD − dE) / Lx += d · cos(θ + Δθ/2) y += d · sin(θ + Δθ/2)θ += ΔθdE, dD em metros; θ em radianos

Como ler: a seta azul é a frente do robô, que aponta na direção θ medida a partir do eixo x. A régua âmbar L é a distância entre as rodas. As cinco linhas do quadro são o algoritmo inteiro: se a direita andou mais que a esquerda, Δθ é positivo e o robô virou para a esquerda.

Objetivo: estimar x, y (em centímetros) e θ (em graus) empurrando o robô com a mão. Mesmos pinos do tópico 4. Ajuste o raio da roda e a distância entre rodas do seu chassi.

odometria.ino
// Odometria de robô diferencial com dois encoders (pacote esp32 3.x)
const int ENC_E_A = 18, ENC_E_B = 19, ENC_D_A = 34, ENC_D_B = 35;
const float PULSOS_VOLTA = 1400;           // meça o seu (tópico 6)
const float RAIO = 0.0215;                 // raio da roda em metros (roda de 43 mm)
const float BASE = 0.130;                  // distância entre as rodas em metros
const float M_POR_PULSO = 2 * PI * RAIO / PULSOS_VOLTA;
volatile long pulsosE = 0, pulsosD = 0;
float x = 0, y = 0, theta = 0;

void IRAM_ATTR isrE() { if (digitalRead(ENC_E_A) == digitalRead(ENC_E_B)) pulsosE++; else pulsosE--; }
void IRAM_ATTR isrD() { if (digitalRead(ENC_D_A) == digitalRead(ENC_D_B)) pulsosD--; else pulsosD++; }

void setup() {
  Serial.begin(115200);
  pinMode(ENC_E_A, INPUT_PULLUP); pinMode(ENC_E_B, INPUT_PULLUP);
  pinMode(ENC_D_A, INPUT); pinMode(ENC_D_B, INPUT);
  attachInterrupt(digitalPinToInterrupt(ENC_E_A), isrE, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_D_A), isrD, CHANGE);
}

void loop() {
  static long antE = 0, antD = 0;
  static unsigned long ultimoPrint = 0;
  delay(50);                                  // passo de 50 ms
  noInterrupts(); long e = pulsosE, d = pulsosD; interrupts();
  float dE = (e - antE) * M_POR_PULSO;
  float dD = (d - antD) * M_POR_PULSO;
  antE = e; antD = d;
  float dist = (dE + dD) / 2;
  float dTheta = (dD - dE) / BASE;
  x += dist * cos(theta + dTheta / 2);
  y += dist * sin(theta + dTheta / 2);
  theta = atan2(sin(theta + dTheta), cos(theta + dTheta));   // mantém entre -180° e 180°
  if (millis() - ultimoPrint >= 250) {
    ultimoPrint = millis();
    Serial.printf("x=%.1f cm  y=%.1f cm  theta=%.1f graus\n", x * 100, y * 100, theta * 180 / PI);
  }
}

Como verificar

  • ✓Empurre o robô 50 cm em linha reta sobre uma trena: x perto de 50, y e theta perto de 0.
  • ✓Gire o robô no lugar um quarto de volta para a esquerda: theta perto de 90.
  • ✗x sai negativo andando para a frente? Os dois encoders estão contando ao contrário (tópico 4).

💡 Giro pela IMU, distância pelas rodas

Roda escorrega mais nas curvas do que em linha reta. Uma melhoria simples: use o encoder para a distância e o giro Z da IMU (integrado, com offset descontado) para o θ. É o começo da fusão de sensores que o ROS 2 faz na Trilha 4.

Por que aprender

Voltar à base, andar até um ponto, desenhar um quadrado no chão: tudo isso precisa de x, y e θ. É exatamente o que o micro-ROS do 4.3 publica como odometria para o ROS 2 montar mapas.

Conceitos-chave

Pose
x, y e θ juntos
Metros por pulso
2πr ÷ pulsos/volta
Base (L)
distância entre rodas
Radiano
π rad = 180°
6

📉 Erros que se acumulam e como conviver com eles

A odometria nunca está certa, só "quase certa". Cada passo carrega um erro pequeno: a roda escorregou, o diâmetro real é 1 mm maior, um pulso se perdeu. Como a posição é a soma de todos os passos, esses erros se acumulam, e o erro de direção é o pior: 2° errados no começo viram 3,5 cm de desvio a cada metro andado. O jeito de conviver é calibrar bem e corrigir de vez em quando com algo que não deriva.

— caminho real- - odometriacomeça igual, abre a cada cantoo erro de ângulo vira erro de posiçãoponto vermelho: onde o robô"acha" que terminou

Como ler: o quadrado azul é o que o robô fez de verdade; a linha ciano é o que a odometria somou. No primeiro lado quase não há diferença. A cada curva, um erro pequeno de ângulo entorta todos os lados seguintes. Por isso o teste de calibração usa voltas completas: o erro aparece grande e fácil de medir.

Calibração em 4 passos

1

Pulsos por volta

Marque a roda com fita, gire 10 voltas com a mão e divida a contagem por 10. Use esse número, não o do anúncio.

2

Raio da roda

Empurre o robô 2 m em linha reta. Se x mostrar 196 cm, multiplique o RAIO por 200/196.

3

Base entre rodas

Gire o robô 5 voltas completas no lugar (1800°). Se theta somado mostrar 1750°, multiplique a BASE por 1750/1800.

4

Teste do quadrado

Faça um quadrado de 1 m nos dois sentidos e meça quanto o robô parou longe do início. Anote: é o seu número de referência.

✓ Reduz o erro

  • ✓Rodas de borracha firme e aceleração suave (sem patinar).
  • ✓θ pelo giroscópio, distância pelos encoders.
  • ✓Zerar a pose em pontos conhecidos (base, marca no chão, parede).

✗ Aumenta o erro

  • ✗Arrancadas bruscas e freadas secas.
  • ✗Tapete fofo, rodas de plástico liso, chão com junta.
  • ✗Confiar na odometria por minutos sem nenhuma correção.

Por que aprender

Saber que a posição "escorrega" muda o jeito de projetar: tarefas curtas confiam na odometria; tarefas longas precisam de referência externa, como a linha no chão do 2.6, a câmera da Trilha 3 ou o LiDAR com SLAM da Trilha 4, que existe justamente para corrigir esse erro.

Conceitos-chave

Erro acumulado
cresce com a distância
Erro sistemático
calibração resolve
Escorregamento
aleatório, só reduz
Referência externa
zera a deriva

✅ Resumo do módulo

✓
IMU = acelerômetro + giroscópio: um acerta na média, o outro no movimento rápido.
✓
I2C direto nos registradores: acordar em 0x6B, ler 14 bytes a partir de 0x3B.
✓
Filtro complementar: 0,98 do giroscópio + 0,02 do acelerômetro, em laço de tempo fixo.
✓
Encoders com interrupção: quadratura dá o sentido; ISR curta com IRAM_ATTR e volatile.
✓
Odometria e seus limites: x, y, θ por soma de passos; calibre e corrija com referência externa.

Para ir além

Próximo módulo

2.3 · Controle PID na prática: usar a velocidade medida pelos encoders para manter as duas rodas iguais.