Tutorial · Iniciante · 22 min
O algoritmo de desvio de obstáculos: sinta, varra, decida, vire
Implemente o laço de desvio de obstáculos com limiar de parada, varredura com servo e uma máquina de estados que escolhe o giro seguro e evita travamentos.
Depois que um robô consegue ler uma distância limpa e procurar a direção livre, resta uma parte: a lógica que decide o que fazer. Este é o cérebro de um robô que desvia de obstáculos — um laço curto que o desloca para a frente, percebe quando algo está perto demais, olha ao redor e gira em direção ao espaço livre. É mais simples do que parece, e acertá-lo depende principalmente de não ficar preso.
Reativo, não planejado
Este robô não tem mapa. Ele reage ao que está à sua frente no momento: sente, decide, age e repete. Isso é navegação reativa, a abordagem inicial adequada — barata, robusta e suficiente para vagar por um cômodo cheio de obstáculos sem colidir. Não é o mesmo que planejamento de caminho, no qual um robô usa um mapa para escolher uma rota antecipadamente (esse é o domínio do A* em uma grade de ocupação). O desvio reativo é a base; o planejamento vem depois.
O limiar de parada
Tudo depende de um número: quão perto é perto demais? Escolha uma distância na qual o robô para e reavalia a situação. Se for pequena, ele colide com os obstáculos antes de conseguir reagir; se for grande, para diante de tudo e avança com excessiva cautela. O valor correto depende da velocidade — um robô mais rápido precisa reagir antes, porque percorre mais distância antes de conseguir parar. Para um robô pequeno em ritmo tranquilo, 15–25 cm é um começo sensato.
A máquina de estados
Um sensor de distância evita a maioria dos obstáculos; um interruptor de contato detecta os que ele não consegue ver, e todo robô que funciona sem supervisão precisa dos dois.
A maneira mais limpa de expressar o comportamento é uma máquina de estados finita: alguns estados nomeados com regras claras para passar de um para outro. Ela substitui um emaranhado de ifs aninhados por algo que você pode ler e depurar. (Para o padrão geral, veja máquinas de estados finitas para o comportamento de robôs.)
enum State { DRIVE, SCAN, TURN, REVERSE };
State state = DRIVE;
const int STOP_CM = 20;
void loop() {
switch (state) {
case DRIVE:
driveForward();
if (medianCm() < STOP_CM) { stopMotors(); state = SCAN; }
break;
case SCAN: {
int a = bestAngle(); // servo sweep from the scan tutorial
long open = distanceAt(a);
if (open < STOP_CM) state = REVERSE; // nowhere to go
else { turnToward(a); state = TURN; }
break;
}
case TURN:
if (turnComplete()) state = DRIVE; // opening cleared, resume
break;
case REVERSE:
backUp(400); // ms
state = SCAN; // then look again
break;
}
}
Escolhendo para qual lado girar
A varredura já fez a parte difícil: ela retorna a direção mais livre. Gire nessa direção. A chamada turnToward(angle) mapeia “livre à esquerda” para um giro à esquerda e “livre à direita” para um giro à direita, usando acionamento diferencial — uma roda para a frente e a outra para trás, para girar aproximadamente no lugar.
Evitando oscilação e becos sem saída
Dois modos de falha prendem robôs com lógica simples, e ambos têm correções simples:
- Alternância entre esquerda e direita em um canto em que as duas direções parecem semelhantes. Adicione um tempo mínimo de giro para que o robô assuma a decisão de girar e a conclua antes de reavaliar a situação em cada laço.
- Entalamento em um beco sem saída, onde todas as direções estão bloqueadas. É para isso que serve o estado REVERSE: recue e faça uma nova varredura. Um pequeno contador de travamentos — recuar mais longe se você já varreu várias vezes sem progresso — elimina as piores armadilhas.
Ajuste: velocidade de deslocamento versus distância de reação
Dois parâmetros determinam se o robô parece suave ou nervoso: a velocidade de deslocamento e o limiar de parada. Ao aumentar a velocidade, você também terá de aumentar o limiar para manter a distância de parada segura — por v·t para a latência e v²/2a para a frenagem — e por isso dobrar a velocidade mais que dobra o limiar. A maneira mais rápida de encontrar um bom par é ajustá-los onde um erro não custa nada — no simulador de desvio de obstáculos — e só então transferir os valores para a implementação real.
Histerese: dois limiares, não um
A máquina de estados acima usa um único STOP_CM, e essa é a primeira coisa a corrigir. Um robô
que permanece próximo de um limiar alterna entre DRIVE e SCAN muitas vezes por segundo, porque
pequenas mudanças na medição invertem o resultado da comparação. O deslocamento fica entrecortado em vez de firme.
const int STOP_CM = 20; // stop when closer than this
const int RESUME_CM = 30; // do not drive again until clearer than this
A diferença entre eles é a histerese, e ela impede que o robô fique alternando entre as duas opções. O mesmo padrão corrige quase toda oscilação orientada por limiar em código de robôs, não apenas esta — um detector de borda, um aviso de bateria baixa, uma aproximação para ancoragem.
Uma regra útil para o tamanho da diferença: torne-a maior que o ruído da medição mais a distância que o robô percorre em um laço. Para um robô a 0,3 m/s com um ciclo de sensor de 60 ms, isso é 18 mm de deslocamento mais alguns centímetros de dispersão dos ecos ultrassônicos — então uma diferença de 10 cm é generosa e uma diferença de 3 cm ainda provoca oscilação.
Assuma a decisão de girar
A segunda oscilação ocorre no nível da decisão: o robô gira para a esquerda, a varredura devolve outro resultado, ele gira para a direita e alterna no lugar. Dois mecanismos corrigem isso:
unsigned long turnStartedAt = 0;
const unsigned long MIN_TURN_MS = 250; // commit for at least this long
case TURN:
if (millis() - turnStartedAt < MIN_TURN_MS) break; // no re-deciding yet
if (medianCm() > RESUME_CM) { stopTurn(); state = DRIVE; }
break;
Um tempo mínimo de giro impede que o robô reavalie a situação antes que a manobra tenha alterado alguma coisa, e a verificação usa o limiar de retomada, não o limiar de parada.
Detectando que o robô está preso
REVERSE resolve um beco sem saída à frente. Ele não resolve o caso mais difícil: um robô
que está formalmente se movendo, varrendo e girando, mas não chegando a lugar algum — um canto
côncavo ou uma cadeira que ele rodeia sem conseguir sair.
uint8_t scanCount = 0;
unsigned long lastProgressAt = 0;
// Called whenever the robot successfully drives forward for a while.
void notedProgress() { scanCount = 0; lastProgressAt = millis(); }
void escalate() {
scanCount++;
if (scanCount >= 3) {
backUp(800); // back further
pivot(random(120, 240)); // and turn a large, RANDOM amount
scanCount = 0;
} else if (millis() - lastProgressAt > 15000) {
backUp(1200);
pivot(180); // full about-turn
lastProgressAt = millis();
}
}
Dois detalhes de projeto merecem ser destacados.
Escalonamento. A primeira resposta a um obstáculo é um giro pequeno; a terceira resposta à mesma situação é um giro grande. Repetir uma resposta fixa é o que mantém um robô preso em um canto.
Aleatoriedade. Um robô determinístico em uma armadilha simétrica faz a mesma escolha a cada vez e entra em laço para sempre. Um componente aleatório na manobra de escape quebra a simetria, e é o motivo pelo qual os primeiros robôs aspiradores usavam deslocamento aleatório com rebote, e não uma regra sofisticada — esse método é realmente robusto contra armadilhas que você não previu.
Lendo os interruptores de contato em todos os estados
A máquina de estados como escrita verifica apenas a distância, e todo sensor de distância tem superfícies que ele não consegue ver. Um robô sem detecção de contato empurra uma cortina até os motores entrarem em travamento.
O contato deve ser tratado acima da máquina de estados, não dentro de um estado:
void loop() {
updateBumpers(); // debounced, from the bump-switch tutorial
// Contact overrides whatever the robot thought it was doing.
if (bumpLeft || bumpRight) {
stopMotors();
backUp(300);
pivot(bumpLeft ? +60 : -60); // turn AWAY from the side that hit
state = SCAN;
return;
}
switch (state) { /* ... as before ... */ }
}
Observe o sinal: gire para longe do interruptor que foi acionado. É exatamente por isso que dois interruptores são mais úteis do que um — um único interruptor central informa que houve uma colisão, mas deixa a direção do giro ao acaso.
Velocidade e limiar, em uma tabela
O limiar de parada não é uma constante; ele é uma função da velocidade, porque a distância de frenagem cresce com o quadrado da velocidade. Para um robô com 80 ms de latência de sensores e decisão, frenando a 1,0 m/s², com raio de 90 mm e margem de 50 mm:
| Velocidade | Termo de latência | Termo de frenagem | Limiar total |
|---|---|---|---|
| 0,15 m/s | 12 mm | 11 mm | 16 cm |
| 0,30 m/s | 24 mm | 45 mm | 21 cm |
| 0,45 m/s | 36 mm | 101 mm | 28 cm |
| 0,60 m/s | 48 mm | 180 mm | 37 cm |
| 0,90 m/s | 72 mm | 405 mm | 62 cm |
Dobrar a velocidade de 0,3 para 0,6 m/s não dobra o limiar — mas o aumenta em 76%, e a 0,9 m/s o robô precisa reagir a mais de meio metro. É por isso que um robô que desvia de forma confiável em velocidade de caminhada começa a bater nas coisas quando você acelera, e por isso que a solução é um limiar dependente da velocidade, em vez de um único valor fixo e grande que torne o robô tímido em todos os lugares.
int stopThresholdCm(float speedMps) {
const float latency = speedMps * 0.08f; // 80 ms
const float braking = (speedMps * speedMps) / 2.0f; // a = 1.0 m/s^2
return (int)((latency + braking + 0.09f + 0.05f) * 100.0f);
}
Imprimir o estado é a ferramenta de depuração
A linha mais útil que você pode adicionar:
if (state != lastReportedState) {
Serial.print(millis()); Serial.print(F(" -> "));
Serial.println(STATE_NAMES[state]);
lastReportedState = state;
}
Imprimir apenas quando o estado muda, e não a cada laço, mantém a saída legível e transforma um robô que
“às vezes se comporta de maneira estranha” em um registro que você pode ler. Quase todo defeito
neste comportamento aparece como uma sequência de estados sem sentido — um TURN que nunca
termina, um SCAN disparando quarenta vezes por segundo, um REVERSE que é reativado
imediatamente.
Se você não consegue responder “o que o robô está fazendo agora” a partir de uma única variável, a máquina de estados não está fazendo seu trabalho.
Solução de problemas
| Sintoma | Causa provável | Correção |
|---|---|---|
| O deslocamento fica intermitente perto de obstáculos | Um único limiar, sem histerese | Pare a 20 cm e retome a 30 cm |
| Alterna entre esquerda e direita no lugar | Está reavaliando a situação durante o giro | Tempo mínimo de giro |
| Entra em laço no mesmo canto para sempre | Escape determinístico | Aplique escalonamento e adicione um componente aleatório |
| Avança contra cortinas e almofadas | O som é absorvido — limitação física | Use interruptores de contato tratados acima da máquina de estados |
| Recua contra as coisas | Não há sensores traseiros | Use um interruptor traseiro ou limite o tempo de ré |
| Desvia bem lentamente, mas colide quando acelera | Limiar fixo e frenagem quadrática | Limiar dependente da velocidade |
| Congela em intervalos aleatórios | Leituras bloqueantes de sensores | Use ping e varredura não bloqueantes |
| Recua imediatamente depois de recuar | REVERSE foi reativado antes de a varredura ser atualizada |
Force uma nova varredura depois de recuar |
| Gira em direção à parede que acabou de atingir | Gira em direção ao interruptor que foi acionado | Gire para longe dele |
| Não é possível saber o que está fazendo | O estado não é impresso | Imprima nas mudanças de estado |
Explore o grafo
Faz parte destas montagens
Projetos e caminhos de aprendizagem que incluem este tutorial.
Leitura complementar