Conteúdo detalhado
🛤️ A pista e a barra de sensores
A pista mais simples é fita isolante preta de 19 mm sobre fundo branco fosco (cartolina, lona ou piso claro), com curvas de raio de pelo menos 10 cm no começo. Na frente do rover vai uma barra com 5 sensores infravermelhos em linha, a 3 a 8 mm do chão, com espaço de 12 a 20 mm entre eles. Com cinco sensores o robô não sabe só "estou na linha": sabe o quanto está fora dela, e é isso que alimenta o PID.
🌱 Novo aqui?
Cada sensor (TCRT5000 ou o par de uma barra QTR) tem um LED infravermelho que ilumina o chão e um fototransistor que mede quanto voltou. O branco reflete muito, o preto absorve. A saída analógica dá um número entre esses extremos, o que permite saber que a linha está "meio" embaixo de um sensor. Use a saída analógica (A0), não a digital (D0).
Como ler: a faixa preta é a linha; os círculos azuis cheios são os sensores que a enxergam (S3 forte, S4 parcial), os vazados veem branco. Como a linha está à direita do centro (S2), o robô precisa virar um pouco para a direita. As barras ciano nas laterais são as rodas.
| Peça | Pino do ESP32 | Observação |
|---|---|---|
| Sensores S0…S4 (A0) | 36, 39, 34, 35, 32 | só ADC1: o ADC2 não funciona com Wi-Fi ligado |
| Motor esquerdo (TB6612FNG) | PWMA 25, AIN1 26, AIN2 27 | STBY no 3V3, VM na bateria |
| Motor direito | PWMB 14, BIN1 16, BIN2 17 | GND comum com o ESP32 |
| Módulo microSD | CS 5, SCK 18, MISO 19, MOSI 23 | igual ao módulo 2.5 |
| Botão e LED | BOOT (0), LED (2) | já vêm na placa |
✓ Pista que ajuda
- ✓Fundo fosco e uniforme; fita bem esticada, sem bolhas.
- ✓Uma reta longa para medir velocidade e um trecho de curvas.
- ✓Barra alimentada em 3,3 V, para a saída não passar do limite do ADC.
✗ Pista que atrapalha
- ✗Piso brilhante ou sol direto: o infravermelho do ambiente cega o sensor.
- ✗Barra alta demais (mais de 1 cm): os sensores veem uma mancha borrada.
- ✗Sensores ligados em 5 V direto no pino do ESP32.
Por que aprender
Metade dos problemas de seguidor de linha é mecânica e óptica, não código. Altura da barra, espaçamento e fundo definem o que o software consegue fazer; vale acertar isso antes da primeira linha de PID.
Conceitos-chave
📐 Calcular o erro da linha (média ponderada)
Dê a cada sensor um "endereço": S0 vale 0, S1 vale 1000, S2 vale 2000, S3 vale 3000, S4 vale 4000. Depois da calibração, cada leitura vira um número de 0 (branco) a 1000 (em cima da linha). A posição da linha é a média dos endereços, pesada pelas leituras: sensores que veem mais preto puxam a média para perto de si. O centro é 2000, então erro = posição − 2000. Erro negativo: linha à esquerda. Positivo: à direita.
🌱 Novo aqui?
Uma média ponderada soma cada valor multiplicado pelo seu peso e divide pela soma dos pesos. Aqui o "valor" é o endereço do sensor e o "peso" é quanto preto ele vê. Fórmula: posição = Σ(leitura × endereço) ÷ Σ(leitura).
Como ler: a altura de cada barra é o quanto o sensor vê de preto; embaixo, o endereço de cada um. A linha tracejada é o centro (S2); a linha laranja é a posição calculada, puxada para S3, que vê mais preto. A distância entre as duas é o erro que o PID vai corrigir.
E se nenhum sensor vê a linha? A soma das leituras fica quase zero e a divisão não faz sentido. O sketch guarda para que lado a linha estava por último e devolve o extremo (0 ou 4000): o robô gira forte para aquele lado até reencontrá-la. Se passar de 0,4 s sem linha, ele para.
Por que aprender
A média ponderada transforma cinco sensores liga/desliga em uma régua contínua de 4001 pontos. Com ela o PID corrige pouco quando o desvio é pequeno e muito quando é grande, e o robô anda liso em vez de ziguezaguear.
Conceitos-chave
🎯 PID de direção somado à velocidade base
O PID do módulo 2.3 entra aqui com uma tarefa só: decidir quanto virar. Ele recebe o erro e devolve uma correção. As duas rodas andam na velocidade base, e a correção é somada a uma e subtraída da outra: esq = base + corr, dir = base − corr. Linha à direita (erro positivo) acelera a roda esquerda e freia a direita, e o robô vira para a direita.
Como ler: siga da esquerda para a direita: a barra mede, o círculo subtrai o centro, o PID decide a correção, e ela se divide em dois sentidos (mais numa roda, menos na outra). A seta ciano fecha a malha: a curva do robô muda a próxima leitura.
| Conta, com o exemplo do tópico 2 | Resultado |
|---|---|
| erro = 2667 − 2000 | +667 |
| P = Kp × erro = 0,08 × 667 | 53 |
| D = Kd × (erro − erro anterior) = 0,6 × (667 − 600) | 40 |
| corr = P + D (Ki = 0 por enquanto) | 93 |
| esq = 150 + 93 · dir = 150 − 93 | 243 · 57 |
💡 O robô foge da linha em vez de seguir?
O sinal está invertido em algum lugar: sensores numerados da direita para a esquerda, motor ligado ao contrário ou linha clara sobre fundo escuro. Não "conserte" com Kp negativo; teste cada parte isolada (Serial Plotter com o robô parado, uma roda de cada vez) e corrija na origem.
Por que aprender
Separar "quão rápido" (base) de "quanto virar" (correção) é o padrão de todo robô com tração diferencial, inclusive o que segue objetos com câmera na Trilha 3 e o que recebe comandos de velocidade no ROS 2.
Conceitos-chave
🧭 Calibração automática dos sensores
Nenhum par de sensores é igual, e a luz da sala muda tudo. Por isso o robô aprende a pista antes de correr: gira no lugar por 2,5 s, passando a barra sobre a linha e o fundo, e guarda o menor e o maior valor de cada sensor. Depois, cada leitura vira (leitura − mínimo) × 1000 ÷ (máximo − mínimo), limitada entre 0 e 1000. Assim os cinco falam a mesma língua.
Como ler: à esquerda, cada barra vai do mínimo (fundo) ao máximo (linha) que aquele sensor mediu: começam e terminam em lugares diferentes. À direita, depois da conta, todos ocupam a mesma faixa de 0 a 1000. Uma barra muito curta à esquerda indica sensor alto demais ou sujo.
Objetivo: o sketch completo do projeto. BOOT uma vez: calibra. Ponha o robô na linha e aperte de novo: segue. Mais uma vez: para e fecha o log. Se o cartão faltar, ele segue sem log. Se a sua barra der valores menores sobre o preto (veja o Serial Monitor na calibração), troque LINHA_ESCURA_ALTA para false.
// Seguidor de linha com PID, calibração automática e log no microSD
// ESP32 (pacote esp32 3.x) + barra de 5 sensores IR analógicos + TB6612FNG + módulo microSD
#include <SPI.h>
#include <SD.h>
// ---------- ajuste aqui ----------
float KP = 0.08, KI = 0.0, KD = 0.6; // ganhos do PID de direção (ponto de partida)
int BASE = 150; // velocidade base (0 a 255)
const bool LINHA_ESCURA_ALTA = true; // a barra lê MAIS alto sobre o preto?
// ---------- pinos ----------
const int SENS[5] = { 36, 39, 34, 35, 32 }; // S0 (esquerda) a S4 (direita), só ADC1
const int PWMA = 25, AIN1 = 26, AIN2 = 27; // motor esquerdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17; // motor direito (STBY no 3V3)
const int SD_CS = 5, BOTAO = 0, LED = 2;
int minimo[5], maximo[5], ultimoLado = 1;
float erroAnt = 0, integral = 0;
bool naLinha = false, temSD = false;
unsigned long tCiclo = 0, tViuLinha = 0;
enum Estado { ESPERA, CALIBRA, PRONTO, SEGUE };
Estado estado = ESPERA;
struct Amostra { uint32_t t; int16_t pos, erro, corr, esq, dir; };
QueueHandle_t filaLog;
File arq;
void motor(int pwm, int in1, int in2, int v) {
v = constrain(v, -255, 255);
digitalWrite(in1, v > 0); digitalWrite(in2, v < 0); ledcWrite(pwm, abs(v)); // API 3.x
}
void andar(int esq, int dir) { motor(PWMA, AIN1, AIN2, esq); motor(PWMB, BIN1, BIN2, dir); }
bool botaoApertado() { static int antes = HIGH; int a = digitalRead(BOTAO); bool b = (antes == HIGH && a == LOW); antes = a; return b; }
void calibrar() { // gira no lugar 2,5 s; é a única espera do programa
for (int i = 0; i < 5; i++) { minimo[i] = 4095; maximo[i] = 0; }
unsigned long t0 = millis();
while (millis() - t0 < 2500) {
bool lado = ((millis() - t0) / 625) % 2; // troca o sentido a cada 0,625 s
andar(lado ? 110 : -110, lado ? -110 : 110);
for (int i = 0; i < 5; i++) { int v = analogRead(SENS[i]); minimo[i] = min(minimo[i], v); maximo[i] = max(maximo[i], v); }
}
andar(0, 0);
for (int i = 0; i < 5; i++) Serial.printf("S%d: min %d max %d\n", i, minimo[i], maximo[i]);
}
int lerPosicao() { // 0 (linha sob S0) a 4000 (sob S4); 2000 = centro
long soma = 0, pesos = 0;
for (int i = 0; i < 5; i++) {
long n = constrain((long)(analogRead(SENS[i]) - minimo[i]) * 1000 / max(maximo[i] - minimo[i], 1), 0L, 1000L);
if (!LINHA_ESCURA_ALTA) n = 1000 - n; // 1000 = em cima da linha
if (n > 50) { soma += n * i * 1000; pesos += n; } // ignora o ruído do fundo
}
naLinha = pesos > 300;
if (!naLinha) return ultimoLado < 0 ? 0 : 4000; // perdeu: vira para onde ela sumiu
int pos = soma / pesos;
ultimoLado = pos < 2000 ? -1 : 1;
return pos;
}
void registrar(uint32_t t, int pos, int erro, int corr, int esq, int dir) {
if (!temSD) return;
Amostra a = { t, (int16_t)pos, (int16_t)erro, (int16_t)corr, (int16_t)esq, (int16_t)dir };
xQueueSend(filaLog, &a, t <= 1 ? pdMS_TO_TICKS(50) : 0); // t = 0 abre arquivo, t = 1 fecha
}
void cicloControle() { // roda a cada 5 ms (200 Hz)
static uint8_t n = 0;
int pos = lerPosicao();
if (naLinha) tViuLinha = millis();
else if (millis() - tViuLinha > 400) { andar(0, 0); return; } // saiu da pista: para
float erro = pos - 2000; // negativo: linha à esquerda
integral = constrain(integral + erro, -20000, 20000); // limita o windup
float corr = KP * erro + KI * integral + KD * (erro - erroAnt);
erroAnt = erro;
int esq = constrain(BASE + corr, -255, 255), dir = constrain(BASE - corr, -255, 255);
andar(esq, dir);
if (++n % 2 == 0) registrar(millis(), pos, erro, corr, esq, dir); // log a 100 Hz
if (n % 10 == 0) Serial.printf("erro:%.0f,corr:%.0f,esq:%d,dir:%d\n", erro, corr, esq, dir); // Plotter a 20 Hz
}
void abrirProximo() { // /pid001.csv, /pid002.csv... um por corrida
char nome[16];
for (int i = 1; i < 1000; i++) { snprintf(nome, sizeof nome, "/pid%03d.csv", i); if (!SD.exists(nome)) break; }
arq = SD.open(nome, FILE_WRITE);
arq.printf("# KP=%.3f KI=%.4f KD=%.3f BASE=%d\n", KP, KI, KD, BASE);
arq.println("t_ms,pos,erro,corr,esq,dir");
Serial.printf("log: %s\n", nome);
}
void tarefaLog(void *p) { // núcleo 0: grava sem atrasar o controle
Amostra a; int linhas = 0;
for (;;) {
if (xQueueReceive(filaLog, &a, portMAX_DELAY) != pdTRUE) continue;
if (a.t == 0) { abrirProximo(); linhas = 0; }
else if (a.t == 1) { if (arq) arq.close(); }
else if (arq) {
arq.printf("%lu,%d,%d,%d,%d,%d\n", (unsigned long)a.t, a.pos, a.erro, a.corr, a.esq, a.dir);
if (++linhas % 50 == 0) arq.flush();
}
}
}
void setup() {
Serial.begin(115200);
pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
ledcAttach(PWMA, 20000, 8); ledcAttach(PWMB, 20000, 8);
pinMode(BOTAO, INPUT_PULLUP); pinMode(LED, OUTPUT);
temSD = SD.begin(SD_CS);
if (temSD) { filaLog = xQueueCreate(64, sizeof(Amostra)); xTaskCreatePinnedToCore(tarefaLog, "log", 4096, NULL, 1, NULL, 0); }
Serial.println(temSD ? "SD ok" : "sem SD: segue sem log");
Serial.println("BOOT: calibrar, depois seguir, depois parar");
}
void loop() {
bool botao = botaoApertado();
switch (estado) {
case ESPERA: if (botao) estado = CALIBRA; break;
case CALIBRA: calibrar(); estado = PRONTO; Serial.println("Calibrado: ponha na linha e aperte BOOT"); break;
case PRONTO:
if (botao) { erroAnt = 0; integral = 0; tViuLinha = millis(); registrar(0, 0, 0, 0, 0, 0); tCiclo = micros(); estado = SEGUE; }
break;
case SEGUE:
if (botao) { andar(0, 0); registrar(1, 0, 0, 0, 0, 0); estado = PRONTO; Serial.println("Parado"); break; }
if (micros() - tCiclo >= 5000) { tCiclo += 5000; cicloControle(); } // 200 Hz, sem delay
break;
}
digitalWrite(LED, estado == SEGUE);
}
Como verificar
- ✓Na calibração, o Serial Monitor mostra 5 linhas como "S3: min 612 max 3580", com distância de pelo menos 1000 entre mínimo e máximo.
- ✓Com o robô suspenso e a linha sob S3, o Plotter mostra erro positivo, esq maior que dir.
- ✓Depois de uma corrida, o cartão tem /pid001.csv com a linha # KP=... no topo.
- ✗Mínimo e máximo quase iguais: barra alta demais, sensor no pino errado ou LED IR desligado (olhe pela câmera do celular: o LED aparece roxo).
Por que aprender
Calibrar no início de cada corrida deixa o mesmo código funcionar em pistas, luzes e sensores diferentes. Em competição, é o que separa o robô que funcionou "na oficina" do que funciona no dia.
Conceitos-chave
📊 Ajuste por telemetria (antes/depois)
"Parece que melhorou" engana. Cada corrida grava um arquivo com os ganhos no cabeçalho, e você compara dois arquivos com dois números: o erro RMS (o tamanho típico do desvio; menor é melhor) e o erro máximo (o pior momento, em geral uma curva). Mude um parâmetro por vez, na mesma pista e com a bateria na mesma carga.
Como ler: o eixo horizontal é o tempo da corrida, o vertical é o erro (zero = linha no centro). A curva vermelha nunca sossega: o robô ziguezagueia até na reta. A ciano fica perto de zero e só sobe nas curvas, voltando em seguida. É esse formato que você procura no PNG.
Passo a passo de sintonia na pista
Só P, devagar
BASE 100, KI 0, KD 0. Suba KP (0,02 → 0,04 → 0,06...) até o robô seguir a pista inteira, mesmo balançando.
D para amortecer
Suba KD (0,2 → 0,4 → 0,8...) até o balanço sumir na reta. Se o robô começar a tremer rápido, voltou demais: desça um passo.
Mais velocidade
Suba BASE de 20 em 20. A cada degrau, refaça um ajuste fino de KP e KD olhando o RMS.
I só se precisar
Se o erro médio fica sempre de um lado (robô "puxado"), um KI pequeno (0,0005) resolve. Na maioria das pistas, KI = 0.
Objetivo: comparar duas corridas em números e num gráfico só. Use o mesmo ambiente Python do módulo 2.5. Para ver uma corrida sozinha, o analisa_log.py do 2.5 já serve: python analisa_log.py pid001.csv erro corr.
# compara.py: antes x depois de um ajuste do PID
# Uso: python compara.py pid001.csv pid002.csv
import sys
import pandas as pd
import matplotlib.pyplot as plt
for arquivo in sys.argv[1:]:
ganhos = open(arquivo).readline().strip("# \n") # linha "# KP=... KD=..."
df = pd.read_csv(arquivo, comment="#")
t = (df["t_ms"] - df["t_ms"].iloc[0]) / 1000
rms = (df["erro"] ** 2).mean() ** 0.5
satur = ((df["esq"].abs() >= 255) | (df["dir"].abs() >= 255)).mean() * 100
print(f"{arquivo}: {ganhos} | RMS {rms:.0f} | máx {df['erro'].abs().max()} | volta {t.iloc[-1]:.2f} s | saturado {satur:.0f}%")
plt.plot(t, df["erro"], label=f"{arquivo} ({ganhos})")
plt.axhline(0, color="gray", lw=0.8)
plt.xlabel("tempo (s)"); plt.ylabel("erro"); plt.legend(); plt.grid(alpha=0.3)
plt.savefig("comparacao.png", dpi=120)
plt.show()
Como verificar
- ✓Uma linha por arquivo, com os ganhos e o RMS: o ajuste bom tem RMS menor e a curva fica mais perto de zero no comparacao.png.
- ✓"saturado" acima de 20% indica BASE alta demais: as rodas já estão no limite e não sobra margem para corrigir.
Por que aprender
Medir antes e depois é o método de engenharia em miniatura: hipótese, uma mudança, dados, conclusão. Os mesmos dois números servem para ajustar o robô com câmera da Trilha 3 e para defender o projeto numa feira ou competição.
Conceitos-chave
🏆 Desafios: curvas fechadas, cruzamentos, cronômetro
Com o robô completando a pista, os desafios abaixo treinam o que as provas de seguidor de linha (como a modalidade prática da OBR) cobram. Cada um é uma mudança pequena no sketch; faça um por vez e compare com o log.
Reduza a base quando o erro cresce: int base = BASE - abs(erro) * 0.04; e use base no lugar de BASE. Reta rápida, curva segura.
Quando os cinco sensores passam de 700 ao mesmo tempo, é um cruzamento: mantenha o último erro por 100 ms e siga reto, sem deixar o PID reagir à "linha larga".
Uma marca lateral na largada, lida por um sexto sensor (GPIO 33, também ADC1): guarde o millis() a cada passagem e imprima o tempo da volta.
✓ Segurança e método
- ✓Primeiro teste de cada mudança com o robô suspenso, depois na pista.
- ✓Mão perto do BOOT nas primeiras voltas; a parada por linha perdida é a segunda proteção.
- ✓Anote no diário de bordo cada arquivo de log e o que mudou.
✗ Armadilhas de prova
- ✗Ajustar com bateria cheia e competir com ela pela metade (ou o contrário).
- ✗Pular a calibração "porque ontem funcionou": a luz do ginásio é outra.
- ✗Deixar LiPo carregando sozinha na bancada durante os testes.
💡 Fechando a Trilha 2
Você tem agora um robô que se controla pelo celular, sabe medir o próprio movimento, roda PID em ritmo fixo, divide o trabalho em tarefas e registra tudo. O limite que sobra é o sensor: cinco pontos de luz no chão. Na Trilha 3 o robô ganha uma câmera e um Raspberry Pi 5, e o mesmo PID passa a seguir objetos e pessoas.
Por que aprender
Desafios pequenos e medidos são a ponte entre "funciona" e "funciona bem". O hábito de mudar uma coisa, gravar e comparar é o que você vai levar para todos os projetos das próximas trilhas.
Conceitos-chave
✅ Resumo do módulo e da Trilha 2
Para ir além
- Biblioteca QTR (Pololu) — a referência clássica de barra de sensores e da média ponderada
- ADC no Arduino-ESP32 — pinos, atenuação e limites
- LEDC (PWM) na API 3.x — ledcAttach e ledcWrite
- Olimpíada Brasileira de Robótica — regras e pistas da modalidade prática
- Wokwi — teste a lógica de estados e o PID com potenciômetros no lugar dos sensores
Próxima trilha
Trilha 3 · Visão e IA embarcada. Começa pelo módulo 3.1: preparar o Raspberry Pi 5 com Linux, o "computador que pensa" ao lado do ESP32 que você já domina.