Conteúdo detalhado
🔁 Por que liga/desliga não basta
O controle mais simples é o liga/desliga: abaixo do alvo, motor no máximo; acima, motor desligado. Funciona num ferro de passar, que esquenta devagar. Num robô, que reage rápido, o resultado é um vaivém sem fim: a roda passa do alvo, freia, fica abaixo, acelera tudo de novo. O seguidor de linha "dança" na pista e perde as curvas. Falta uma resposta proporcional: corrigir pouco quando o erro é pequeno e muito quando é grande.
🌱 Novo aqui?
O alvo (setpoint) é o valor que você quer, como 150 RPM. A medida é o que o sensor diz que está acontecendo. O erro é alvo menos medida. A saída é o que o controlador manda para o atuador, aqui o PWM do motor. Malha fechada é esse ciclo em que a medida volta para corrigir a saída.
Como ler: a linha âmbar é o alvo nos dois gráficos. À esquerda, a velocidade bate no alvo e já passa, porque o motor só conhece "tudo" ou "nada". À direita, o PID reduz a força conforme chega perto: um pequeno passo além do alvo e depois fica em cima dele.
✓ Liga/desliga serve
- ✓Sistemas lentos: aquecedor, bomba de caixa-d'água.
- ✓Decisões de estado: parar ao ver obstáculo.
✗ Precisa de PID
- ✗Velocidade de roda, direção em seguidor de linha.
- ✗Equilíbrio, posição de braço, giro de exatos 90°.
Por que aprender
O PID está no controle de velocidade de carro, no drone, na impressora 3D e no braço da Trilha 5. Entender por que o liga/desliga falha é entender o problema que o PID resolve.
Conceitos-chave
🏎️ P, I e D explicados com o carro na pista
Imagine que você dirige e quer ficar no meio da faixa. O P (proporcional) é girar o volante conforme a distância do meio: longe, gira muito; perto, pouco. Só com ele, você zigue-zagueia. O D (derivativo) olha a velocidade com que você se aproxima do meio e alivia o volante antes de passar: amortece o zigue-zague. O I (integral) percebe que, com vento lateral, você sempre fica um pouco à direita, e vai somando esse erro até compensar.
🌱 Novo aqui?
A integral do erro é a soma do erro ao longo do tempo: erro pequeno que não some vira uma soma grande. A derivada do erro é a rapidez com que ele muda. Os ganhos Kp, Ki e Kd são os três números que você ajusta: quanto peso dar a cada parte. Saída = Kp·erro + Ki·soma + Kd·variação.
Como ler: as três curvas respondem ao mesmo pedido de "acelere até o alvo". A cinza (só P) para antes: sobra um erro fixo. A ciano (P + I) elimina esse erro, mas a soma passa do ponto e balança. A azul (PID) usa o D para frear a chegada e assenta mais cedo.
| Aumentar… | Velocidade de resposta | Passa do alvo | Erro que sobra |
|---|---|---|---|
| Kp | mais rápida | mais | diminui, não some |
| Ki | um pouco mais rápida | mais | some |
| Kd | pouco muda | menos | não muda (amplifica ruído) |
Por que aprender
Quem entende o papel de cada letra sintoniza olhando o gráfico: "passou demais, mais D"; "ficou abaixo, mais I". Quem não entende fica trocando números ao acaso.
Conceitos-chave
💻 PID em código (loop de tempo fixo)
Em código, o PID é uma função chamada sempre no mesmo intervalo, como a cada 10 ms. O intervalo fixo importa: a integral soma erro × tempo e a derivada divide por tempo; se o intervalo variar, os ganhos mudam de efeito sozinhos. Por isso o laço usa micros() e um "próximo instante" marcado, em vez de delay().
Como ler: siga as setas azuis da esquerda para a direita: alvo, erro, PID, PWM, motor. A seta ciano tracejada é a medida voltando. No código abaixo, o "motor + encoder" é uma simulação de duas linhas, para você brincar com os ganhos sem hardware; no tópico 6 ela vira o motor de verdade.
Objetivo: rodar um PID completo a 100 Hz contra um motor simulado, com alvo alternando entre 120 e 220 RPM a cada 4 s e uma "subida" que freia a roda a partir de 12 s. Você muda os ganhos digitando no Serial Monitor (ex.: p3, i5, d0.05). Roda no Wokwi com um ESP32 sozinho.
// PID contra um motor simulado: entenda os ganhos sem hardware (pacote esp32 3.x)
struct PID {
float kp = 1.0, ki = 0.0, kd = 0.0; // comece só com P
float saidaMin = 0, saidaMax = 255; // limites do PWM
float integral = 0, dFiltrada = 0, medidaAnt = 0;
bool primeira = true;
float calcula(float alvo, float medida, float dt) {
float erro = alvo - medida;
if (primeira) { medidaAnt = medida; primeira = false; }
float dBruta = -(medida - medidaAnt) / dt; // derivada da MEDIDA: sem tranco quando o alvo muda
medidaAnt = medida;
dFiltrada += 0.2 * (dBruta - dFiltrada); // filtro passa-baixa na derivada
float novaIntegral = integral + ki * erro * dt;
float saida = kp * erro + novaIntegral + kd * dFiltrada;
if (saida > saidaMax) { saida = saidaMax; if (erro < 0) integral = novaIntegral; } // anti-windup
else if (saida < saidaMin) { saida = saidaMin; if (erro > 0) integral = novaIntegral; }
else integral = novaIntegral;
return saida;
}
};
PID pid;
const unsigned long PERIODO_US = 10000; // 10 ms = 100 Hz
const float DT = PERIODO_US / 1e6;
float vel = 0; // velocidade simulada, em RPM
void motorSimulado(float pwm) { // motor de 1ª ordem: tende a 1,2 x PWM em ~0,3 s
float carga = (millis() > 12000) ? 40 : 0; // a partir de 12 s, uma subida freia a roda
vel += ((1.2 * pwm - carga) - vel) * DT / 0.3;
}
void setup() {
Serial.begin(115200);
Serial.setTimeout(20);
Serial.println("Digite p, i ou d seguido do valor. Ex.: p3");
}
void loop() {
static unsigned long proximo = micros();
if ((long)(micros() - proximo) < 0) return; // ainda não chegou o próximo instante
proximo += PERIODO_US;
if (Serial.available()) {
char g = Serial.read();
float v = Serial.parseFloat();
if (g == 'p') pid.kp = v; else if (g == 'i') pid.ki = v; else if (g == 'd') pid.kd = v;
}
float alvo = ((millis() / 4000) % 2) ? 220 : 120;
float pwm = pid.calcula(alvo, vel, DT);
motorSimulado(pwm);
static int n = 0;
if (++n % 5 == 0) Serial.printf("alvo:%.0f,vel:%.1f,pwm:%.0f\n", alvo, vel, pwm);
}
Como verificar
- ✓No Serial Plotter, com só P = 1: "vel" fica bem abaixo de "alvo" (erro que sobra).
- ✓Digite i5: "vel" encosta no alvo; após 12 s, a carga puxa a velocidade para baixo e o I a traz de volta.
- ✓Digite p8: a resposta fica nervosa e passa do alvo. Volte para p2.
Por que aprender
Esta estrutura de PID é a que você vai copiar para o seguidor de linha do 2.6, para o giro do robô que segue objetos no 3.6 e para qualquer coisa que precise "chegar e ficar". Testar com simulação primeiro separa erro de código de erro de montagem.
Conceitos-chave
🎛️ Sintonia passo a passo (Ziegler-Nichols simplificado e na mão)
Sintonizar é escolher Kp, Ki e Kd para o seu robô. Não existe número universal: depende do motor, da roda, do peso e da bateria. Há dois caminhos que se completam. O método Ziegler-Nichols dá um ponto de partida a partir de um único teste; o ajuste na mão, olhando o Serial Plotter, faz o acabamento.
Ajuste na mão, na ordem certa
Zere I e D
Comece com i0 e d0. Robô suspenso, rodas no ar.
Suba o P até quase oscilar
Dobre o Kp até a resposta começar a balançar; então volte para a metade desse valor.
Adicione I devagar
Suba o Ki até o erro que sobrava sumir em menos de um segundo, sem balançar muito.
D só se precisar
Se passar muito do alvo, um pouco de Kd amortece. Em velocidade de roda, muitas vezes PI basta.
Teste no chão e anote
Com o robô no chão o peso muda a resposta; ajuste de novo e anote os ganhos finais no código.
Ziegler-Nichols simplificado: com I e D em zero, suba o Kp até a velocidade oscilar num ritmo constante, sem crescer nem morrer. Esse é o ganho crítico Ku. Meça o tempo de uma oscilação completa no Serial Plotter: é o período Tu, em segundos. A tabela dá os ganhos iniciais.
| Controlador | Kp | Ki | Kd |
|---|---|---|---|
| P | 0,5 · Ku | 0 | 0 |
| PI | 0,45 · Ku | 0,54 · Ku / Tu | 0 |
| PID | 0,6 · Ku | 1,2 · Ku / Tu | 0,075 · Ku · Tu |
💡 Ziegler-Nichols é agressivo
Os ganhos da tabela costumam dar uma resposta rápida que passa bastante do alvo. Use-os como ponto de partida e reduza Kp e Ki uns 20 a 30% se o robô ficar nervoso. A conta serve para sair do zero, não para dispensar o teste.
✓ Sintonia que dá certo
- ✓Mudar um ganho por vez e olhar o gráfico.
- ✓Mudar em passos grandes (dobrar, dividir por 2).
- ✓Sintonizar com a bateria carregada como no uso real.
✗ Sintonia que enlouquece
- ✗Mexer nos três ganhos ao mesmo tempo.
- ✗Ajustar "no olho", sem Serial Plotter.
- ✗Copiar os ganhos de um robô diferente do seu.
Por que aprender
Um PID mal sintonizado é pior que nenhum: oscila, gasta bateria e esquenta o motor. Com um método, a sintonia de um seguidor de linha cai de uma tarde de tentativa e erro para alguns testes organizados.
Conceitos-chave
🧯 Windup, saturação e filtro na derivada
O PID "de livro" tem três armadilhas no mundo real, e o código do tópico 3 já trata as três. Saturação: o PWM não passa de 255; pedir 400 dá na mesma. Windup: enquanto a saída está presa no máximo, a integral continua somando erro; quando o robô finalmente chega, essa soma gigante o faz passar muito do alvo. Ruído na derivada: o D amplifica cada pulinho da medida e faz o motor tremer.
Como ler: na faixa vermelha a roda está segura pela mão: velocidade zero, saída no máximo. Quando você solta, a curva vermelha (sem proteção) dispara para muito além do alvo, gastando a integral acumulada. A azul (com proteção) não acumulou nada enquanto estava saturada e chega limpa.
| Problema | Linha do código (tópico 3) | O que faz |
|---|---|---|
| Saturação | saidaMin / saidaMax | corta a saída na faixa que o motor aceita |
| Windup | if (erro < 0) integral = novaIntegral | na saturação, só aceita a integral se ela ajudar a sair dela |
| Tranco no alvo | -(medida - medidaAnt) / dt | derivada da medida, não do erro: mudar o alvo não dá pico |
| Ruído no D | dFiltrada += 0.2 * (...) | média móvel exponencial: suaviza antes de multiplicar por Kd |
🌱 Novo aqui?
Um filtro passa-baixa deixa passar a mudança lenta e corta a rápida, como a média do 1.4. A forma f += a * (novo - f) faz isso com uma linha: com a = 0.2, cada leitura nova puxa o valor filtrado só 20% na direção dela. Menor a = mais suave e mais atrasado.
⚠️ Teste o windup com cuidado
Para ver o efeito no rover real, segure a roda só com o motor pequeno (N20 ou TT) e por poucos segundos: motor travado puxa a corrente máxima e esquenta o motor e o driver. Nunca faça isso com motores maiores.
Por que aprender
A diferença entre um PID que funciona na simulação e um que funciona no robô está nestas quatro linhas. Bibliotecas prontas de PID fazem o mesmo; agora você sabe o que procurar nelas.
Conceitos-chave
⚖️ Velocidade igual nas duas rodas com PID
Dois motores "iguais" nunca são iguais: com o mesmo PWM, um gira mais que o outro e o rover puxa para um lado. A solução é dar um PID para cada roda, os dois com o mesmo alvo em RPM, cada um medindo o próprio encoder. O PID da roda mais fraca simplesmente manda mais PWM até as duas baterem.
Como ler: o mesmo alvo âmbar alimenta dois laços que não se conhecem. Cada linha ciano tracejada é o encoder de uma roda voltando para o seu próprio PID. Se a roda direita for mais fraca, só o laço de baixo aumenta o PWM.
Objetivo: manter as duas rodas no RPM que você digitar no Serial Monitor (ex.: 150; 0 para parar), com um PI em cada uma a 50 Hz. Pinos do driver iguais aos do 2.1 e dos encoders iguais aos do 2.2.
// Duas rodas na mesma velocidade: um PID por roda (TB6612FNG + encoders, pacote esp32 3.x)
const int PWMA = 25, AIN1 = 26, AIN2 = 27, PWMB = 14, BIN1 = 16, BIN2 = 17; // STBY no 3V3
const int ENC_E_A = 18, ENC_E_B = 19, ENC_D_A = 34, ENC_D_B = 35;
const float PULSOS_VOLTA = 1400; // o valor medido no 2.2
const unsigned long PERIODO_US = 20000; // 20 ms = 50 Hz
const float DT = PERIODO_US / 1e6;
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++; }
struct PID {
float kp = 0.8, ki = 4.0, kd = 0.0, saidaMin = 0, saidaMax = 255; // ponto de partida: ajuste o seu
float integral = 0, dFiltrada = 0, medidaAnt = 0;
float calcula(float alvo, float medida, float dt) {
float erro = alvo - medida;
dFiltrada += 0.2 * (-(medida - medidaAnt) / dt - dFiltrada);
medidaAnt = medida;
float novaIntegral = integral + ki * erro * dt;
float saida = kp * erro + novaIntegral + kd * dFiltrada;
if (saida > saidaMax) { saida = saidaMax; if (erro < 0) integral = novaIntegral; }
else if (saida < saidaMin) { saida = saidaMin; if (erro > 0) integral = novaIntegral; }
else integral = novaIntegral;
return saida;
}
void zera() { integral = 0; dFiltrada = 0; }
};
PID pidE, pidD;
float alvoRPM = 0, rpmE = 0, rpmD = 0;
void frente(int pwm, int in1, int in2, int v) { digitalWrite(in1, v > 0); digitalWrite(in2, LOW); ledcWrite(pwm, v); }
void setup() {
Serial.begin(115200);
Serial.setTimeout(20);
int saidas[] = {AIN1, AIN2, BIN1, BIN2};
for (int p : saidas) pinMode(p, OUTPUT);
ledcAttach(PWMA, 20000, 8);
ledcAttach(PWMB, 20000, 8);
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("Digite o alvo em RPM (ex.: 150). 0 para parar.");
}
void loop() {
static unsigned long proximo = micros();
static long antE = 0, antD = 0;
if ((long)(micros() - proximo) < 0) return;
proximo += PERIODO_US;
if (Serial.available()) alvoRPM = constrain(Serial.parseFloat(), 0, 250);
noInterrupts(); long e = pulsosE, d = pulsosD; interrupts();
rpmE = 0.7 * rpmE + 0.3 * ((e - antE) / PULSOS_VOLTA / DT * 60); // filtro leve na medida
rpmD = 0.7 * rpmD + 0.3 * ((d - antD) / PULSOS_VOLTA / DT * 60);
antE = e; antD = d;
if (alvoRPM < 1) { // parar de verdade e limpar a integral
frente(PWMA, AIN1, AIN2, 0); frente(PWMB, BIN1, BIN2, 0);
pidE.zera(); pidD.zera();
} else {
frente(PWMA, AIN1, AIN2, (int)pidE.calcula(alvoRPM, rpmE, DT));
frente(PWMB, BIN1, BIN2, (int)pidD.calcula(alvoRPM, rpmD, DT));
}
static int n = 0;
if (++n % 5 == 0) Serial.printf("alvo:%.0f,esq:%.1f,dir:%.1f\n", alvoRPM, rpmE, rpmD);
}
Como verificar
- ✓Rodas no ar, digite 150: no Serial Plotter, "esq" e "dir" sobem e se encontram em 150 em menos de um segundo.
- ✓Encoste o dedo de leve numa roda: ela cai um pouco e volta para 150 sozinha.
- ✓No chão, com 150 nas duas, o rover anda reto por 2 m (compare com PWM fixo).
- ✗Uma roda dispara para o máximo e não volta? O encoder dela conta ao contrário: o PID "vê" a roda indo para trás e acelera mais.
Por que aprender
Andar reto parece trivial até você tentar. Com velocidade controlada nas duas rodas, a odometria do 2.2 fica mais confiável, o seguidor de linha do 2.6 tem uma base estável para somar a correção de direção, e o comando /cmd_vel do ROS 2 na Trilha 4 vira exatamente isso: um alvo de velocidade por roda.
Conceitos-chave
✅ Resumo do módulo
Para ir além
- Improving the Beginner's PID (Brett Beauregard) — a série que inspirou a biblioteca PID do Arduino
- Control Tutorials for MATLAB and Simulink: PID — efeito de cada ganho com gráficos (Univ. de Michigan)
- Serial Plotter da Arduino IDE 2 — como plotar várias variáveis
- LEDC (PWM) no arduino-esp32 — ledcAttach e ledcWrite na 3.x
Próximo módulo
2.4 · Várias tarefas ao mesmo tempo: millis(), máquina de estados e FreeRTOS para o robô ler, decidir e controlar sem travar.