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:
- 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.
- 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.
- 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:
- Coloque o robô sobre blocos para que as rodas girem livremente.
- 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.
- Defina o PWM em 100, 150, 200, 250 e registre a velocidade constante em cada caso.
- 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
- 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.
- 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.
- 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.
- Teste sob carga, não sobre blocos. O ponto é verificar a roda cujo comando excede o que ela consegue entregar sem saturar.
- 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