PTENES
MÓDULO 2.3 · FUNDAMENTO

🎯 Controle PID na prática

O PID é a fórmula que faz um robô chegar ao alvo sem passar do ponto e ficar lá, mesmo com bateria fraca ou rampa no caminho. Você vai entender cada letra com um carro na pista, escrever o PID em código, testar num motor simulado sem hardware, sintonizar os ganhos e terminar com as duas rodas do rover girando na mesma velocidade.

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

Conteúdo detalhado

1

🔁 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.

liga/desligaPIDoscila para semprepassa um pouco e assentaalvo

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

Alvo
o que você quer
Erro
alvo − medida
Oscilação
vaivém em volta do alvo
Malha fechada
a medida corrige a saída
2

🏎️ 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.

alvosó P: fica abaixoP + I: chega, oscilaP + I + D: chega e assentatempo →

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 respostaPassa do alvoErro que sobra
Kpmais rápidamaisdiminui, não some
Kium pouco mais rápidamaissome
Kdpouco mudamenosnã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

P
reage ao erro de agora
I
corrige o erro que insiste
D
freia a chegada
Overshoot
quanto passa do alvo
3

💻 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().

alvo −PID motorencoder erroPWMrodavelocidade medida · a cada 10 ms

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-simulado.ino
// 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

Período fixo
dt sempre igual
micros()
relógio sem travar
Planta simulada
testar sem motor
Serial Plotter
ver a resposta
4

🎛️ 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

1

Zere I e D

Comece com i0 e d0. Robô suspenso, rodas no ar.

2

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.

3

Adicione I devagar

Suba o Ki até o erro que sobrava sumir em menos de um segundo, sem balançar muito.

4

D só se precisar

Se passar muito do alvo, um pouco de Kd amortece. Em velocidade de roda, muitas vezes PI basta.

5

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.

ControladorKpKiKd
P0,5 · Ku00
PI0,45 · Ku0,54 · Ku / Tu0
PID0,6 · Ku1,2 · Ku / Tu0,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

Ku
ganho que oscila
Tu
período da oscilação
P → I → D
a ordem do ajuste
Um por vez
para saber o efeito
5

🧯 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.

roda presa:integral crescesem anti-windup: passa muitocom anti-windup: chega e ficaalvotempo → (a roda é solta no fim da faixa vermelha)

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.

ProblemaLinha do código (tópico 3)O que faz
SaturaçãosaidaMin / saidaMaxcorta a saída na faixa que o motor aceita
Windupif (erro < 0) integral = novaIntegralna saturação, só aceita a integral se ela ajudar a sair dela
Tranco no alvo-(medida - medidaAnt) / dtderivada da medida, não do erro: mudar o alvo não dá pico
Ruído no DdFiltrada += 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

Saturação
limite físico da saída
Anti-windup
integral sob controle
D na medida
sem pico ao mudar o alvo
Passa-baixa
corta o ruído
6

⚖️ 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.

alvo150 RPM PID esq.PID dir. motor esq.motor dir. enc. esq.enc. dir.

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-pid.ino
// 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

Um PID por roda
laços independentes
RPM medido
pulsos ÷ tempo
PI basta
em velocidade de roda
Zerar a integral
ao parar

✅ Resumo do módulo

✓
Liga/desliga oscila: sistemas rápidos precisam de correção proporcional ao erro.
✓
P reage, I insiste, D freia: saída = Kp·erro + Ki·soma + Kd·variação.
✓
Laço de tempo fixo: micros() e próximo instante, nunca delay() no controle.
✓
Sintonia com método: P, depois I, depois D; Ziegler-Nichols como ponto de partida.
✓
PID de verdade: saturação, anti-windup, derivada da medida filtrada, um laço por roda.

Para ir além

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.