Tutorial · Intermediário · 35 min

Controle da velocidade das rodas em malha fechada com encoders

PWM representa tensão, não velocidade. Monte um controle de velocidade por roda com pré-ação e PI, escolha a taxa de amostragem e mantenha o robô na trajetória.

PWM é uma tensão, não uma velocidade

analogWrite(motorPin, 180) não solicita uma velocidade. Ele solicita cerca de 70% da tensão da bateria, e a velocidade resultante depende da carga sobre a roda, do atrito da caixa de engrenagens, do estado de carga e da temperatura das escovas.

É por isso que um robô com dois motores idênticos e PWM idênticos faz um arco. Eles nunca foram idênticos — uma caixa de engrenagens tem folga diferente, um conjunto de escovas é mais novo e, na mesma tensão, eles giram com uma diferença de 3–8%. Em três metros, isso forma uma curva visível.

Em um robô de tração diferencial, você pode mascarar o efeito com uma constante de ajuste. Em uma base mecanum, isso não funciona, porque a cinemática inversa atribui às quatro rodas alvos muito diferentes — no exemplo prático daquele tutorial, uma roda recebe 0,020 m/s e outra 0,580 m/s a partir do mesmo comando — e são essas proporções que determinam a direção do robô. Uma roda 5% mais lenta não deixa o robô apenas um pouco mais lento; faz com que ele vá para outro lugar.

Fechar o circuito sobre a velocidade medida de cada roda corrige isso, e é a mesma solução para qualquer robô com mais de duas rodas ou que precise chegar com precisão a algum destino.

Meça a velocidade que você realmente tem

Comece pelas contagens de encoder em quadratura. A conversão é uma geometria fixa:

counts per wheel revolution = CPR_motor × gear ratio × 4     (×4 for quadrature edges)
metres per count            = 2πr / counts per revolution

Para um encoder de motor de 12 CPR acoplado a uma caixa de 30:1, em rodas de 97 mm: 12 × 30 × 4 = 1440 contagens/volta, e 2π × 0,0485 / 1440 = 212 µm por contagem.

Depois, a parte que todo mundo faz errado:

// DON'T: counts since last loop, divided by loop time.
float speed = (count - lastCount) * METRES_PER_COUNT / dt;

A 100 Hz e 0,05 m/s, isso equivale a uma contagem por amostra. Seu sinal de velocidade é 0, 212, 0, 212 µm — uma onda quadrada com ondulação de ±100%, e o termo D a amplificará até um motor que zune. Há três saídas, e normalmente você quer a primeira:

  1. Calcule a velocidade a uma frequência menor que a do controle. Meça-a ao longo de 20 ms (50 Hz) e execute o ciclo a 50 Hz. Isso aumenta as contagens por janela, reduz o ruído de quantização e não acrescenta atraso de filtragem.
  2. Meça o tempo entre as bordas, em vez das bordas em um intervalo de tempo. Excelente em baixa velocidade, inconveniente em alta velocidade e dependente dos carimbos de tempo registrados pela interrupção.
  3. Aplique um passa-baixa à velocidade. É a opção mais barata, mas custa margem de fase — justamente aquilo que você tem menos.
// A 50 Hz velocity estimate, with the counter read atomically because an
// encoder ISR is writing it.
noInterrupts();
long count = encoderCount;
interrupts();

float dt = (now - lastTime) * 1e-6f;             // micros(), so no ms rounding
float v = (count - lastCount) * METRES_PER_COUNT / dt;
lastCount = count; lastTime = now;

volatile long encoderCount e o bloco com noInterrupts() não são opcionais: uma leitura de 32 bits ocupa várias instruções em um AVR, e uma ISR que interrompe a leitura no meio pode produzir um valor inconsistente.

Pré-ação primeiro, PID depois

O instinto é recorrer ao PID. Faça primeiro os 90% fáceis.

A velocidade de regime de um motor CC é aproximadamente linear em relação à tensão aplicada, portanto, se você conhece a velocidade desejada, pode calcular a maior parte do PWM:

pwm ≈ kS·sign(v) + kV·v
  • kV — a inclinação. Quanto PWM cada m/s exige.
  • kS — o termo independente. O PWM necessário apenas para vencer o atrito estático, antes de a roda se mover.

Meça ambos em dez minutos:

  1. Coloque o robô sobre blocos para que as rodas girem livremente.
  2. Aumente o PWM lentamente até a roda começar a girar. Esse é o kS, normalmente entre 25 e 45 na escala de 0 a 255.
  3. Defina o PWM em 100, 150, 200, 250 e registre a velocidade constante em cada caso.
  4. Ajuste uma reta aos dados. Sua inclinação é kV e ela deve extrapolar de volta para aproximadamente o seu kS.

Agora o circuito só precisa corrigir o erro entre essa estimativa e a realidade — que é pequeno, de modo que os ganhos também podem ser pequenos e tudo fica estável e silencioso:

float feedforward = kS * sign(target) + kV * target;
float output = feedforward + kP * error + kI * integral;

Sem pré-ação, o integrador precisa construir o comando completo a partir de zero sempre que a referência muda. Isso é lento, provoca sobrepasso e explica por que tantos circuitos de controle dos motores ajustados à mão terminam com um kI enorme e um robô que dá saltos.

O controlador

P e I. Não D.

struct WheelLoop {
  float kS, kV, kP, kI;
  float integral = 0, target = 0;

  int update(float measured, float dt) {
    float error = target - measured;
    float ff = kS * (target > 0 ? 1 : target < 0 ? -1 : 0) + kV * target;
    float unclamped = ff + kP * error + kI * (integral + error * dt);
    int   output    = (int)constrain(unclamped, -255, 255);

    // Conditional integration: only accumulate if the output is NOT saturated,
    // or if the error would wind the integrator back toward the middle.
    // Otherwise a stalled wheel builds a command it cannot use and then keeps
    // driving for a second after you let go of the stick.
    if (fabsf(unclamped) < 255.0f || (error * unclamped) < 0) integral += error * dt;

    return output;
  }
};

Por que não usar derivativo: o termo D diferencia o sinal de velocidade, que já é uma diferença das contagens do encoder. Você diferencia duas vezes, o que significa amplificar duas vezes o ruído de quantização e, em uma roda, quase não há nada para amortecer. Um circuito de velocidade com PI e boa pré-ação é a resposta padrão, e adicionar D é uma das formas mais comuns de piorar um circuito que funciona. (Um circuito de posição é diferente — consulte controle em cascata.)

O anti-windup é a parte que você não pode ignorar. Mantenha o robô pressionado contra uma parede e aplique um comando de velocidade; um integrador sem proteção cresce até a escala máxima em poucos segundos. Solte o robô da parede e ele dispara. A integração condicional, um único if, resolve isso.

Ajuste na ordem correta

  1. Somente pré-ação. kP = kI = 0. Comande 0,3 m/s e meça o resultado. Uma diferença de cerca de 10% significa que kV está correto. Corrija antes de mexer em qualquer outra coisa.
  2. Aumente kP até zumbir e então reduza em um terço. Você está procurando um chiado que aparece com o ganho e desaparece sem ele.
  3. Aumente kI até o erro em regime permanente desaparecer. Uma roda com pré-ação correta precisa de um kI muito pequeno. Comece com kP/10 por segundo e aumente lentamente.
  4. Teste sob carga, não sobre blocos. O ponto é verificar a roda cujo comando excede o que ela consegue entregar sem saturar.
  5. Teste as quatro juntas, dirigindo na diagonal. Esse é o comando com a maior diferença entre os alvos das rodas, e é onde uma roda mal ajustada aparece primeiro.

O simulador de PID é uma forma mais rápida de desenvolver a intuição para as etapas 2 e 3 do que um robô, e o guia de ajuste apresenta o procedimento geral.

Taxa de amostragem

A frequência de amostragem do ciclo precisa ser bem superior à frequência característica do processo que ele controla. A velocidade de um pequeno motor CC com redutor se estabiliza com uma constante de tempo de aproximadamente 50–150 ms; a regra usual é obter dez amostras dentro dessa constante, portanto 50–200 Hz.

Taxa Avaliação
10 Hz Lenta demais. O motor termina de responder antes que o ciclo perceba.
50 Hz O mínimo prático para um conjunto motriz de hobbista e oferece uma janela de encoder de 20 ms.
100 Hz Confortável. Quatro circuitos, mais a cinemática, cabem facilmente em 10 ms em um Uno.
1 kHz A quantização do encoder domina. Agora você está controlando ruído.

Execute o circuito em um intervalo fixo, não “tão rápido quanto loop() permitir”, porque dt aparece diretamente tanto na estimativa de velocidade quanto no integrador:

const unsigned long PERIOD_US = 10000;   // 100 Hz
static unsigned long next = 0;

void loop() {
  unsigned long now = micros();
  if ((long)(now - next) < 0) return;    // signed compare survives rollover
  next += PERIOD_US;
  // ... read encoders, run four loops, write four PWMs
}

A comparação com sinal é a mesma armadilha de transbordamento de millis() em sua forma com micros(), na qual o problema surge após 71 minutos, em vez de 49 dias.

O que isso entrega, medido

Mesmo robô, mesmo comando, mesmo piso:

Malha aberta (PWM) Malha fechada (velocidade)
Desvio ao longo de 3 m 80–200 mm, varia com a bateria Menos de 20 mm, reproduzível
Velocidade quando a bateria cai de 8,4 → 7,0 V Cai cerca de 17% Inalterada até os motores saturarem
Velocidade em uma rampa de 5° Cai abruptamente Mantida, até o torque acabar
Precisão diagonal com mecanum Baixa — as proporções entre as rodas estão erradas Correta, porque são as proporções que são mantidas
Modo de falha quando a operação fica insustentável Fica silenciosamente mais lento Satura, e você pode detectá-lo

A última linha é subestimada. Um circuito fechado fornece um sinal claro de falha: uma roda cuja saída está fixada em 255 e ainda não alcança a referência está informando algo útil, e você pode registrar isso em uma linha de telemetria, recusar o comando ou desacelerar todo o robô para manter as proporções corretas.

Solução de problemas

Sintoma Causa Correção
A roda zumbe ou chia em repouso kP alto demais ou termo D sobre uma velocidade ruidosa Reduza kP; elimine o termo D
O robô dá um solavanco após ficar parado Windup integral Use integração condicional
A velocidade oscila ao redor da referência kI alto demais para a taxa de amostragem Divida kI por dois; verifique se o ciclo está realmente operando na taxa que você pensa
A velocidade alterna entre 0 e o dobro Janela do encoder curta demais para essa velocidade Amostre em uma janela maior ou conte bordas entre carimbos de tempo
Uma roda sempre satura na diagonal Essa roda recebe a maior solicitação; o robô está recebendo comandos excessivos Escale o comando inteiro, não o recorte — consulte a cinemática
As contagens saltam ou retrocedem Bordas perdidas ou leitura não atômica de um volatile long Ative interrupções nos dois canais; proteja a leitura com noInterrupts()
Funciona sobre blocos, mas falha no chão Ajustado sem carga Reajuste durante a condução, não com as rodas livres girando

Próximo passo

As rodas agora fornecem as velocidades solicitadas. Dirija o conjunto na direção pretendida com direção orientada pelo campo e forneça referências suaves em vez de saltos com perfis de movimento. Depois veja o que um circuito de velocidade pode e não pode evitar no simulador de tração mecanum — uma velocidade de roda perfeitamente mantida ainda não significa nada se o piso ceder.

Explore o grafo

Faz parte destas montagens

Projetos e caminhos de aprendizagem que incluem este tutorial.

Leitura complementar

Referências