Tutorial · Intermediário · 14 min de leitura
Detecção de interseções em labirintos de linhas com um arranjo IR
Como um seguidor de linha distingue interseções em T, cruzamentos, becos sem saída e a chegada, e por que uma única leitura do sensor nunca basta para decidir.
Um seguidor de linha tem uma função: manter a linha centralizada. Um robô de labirinto de linhas tem duas, e a segunda é onde os iniciantes travam. Ele ainda precisa seguir a linha, mas também perceber os momentos em que a linha oferece uma escolha — e decidir qual é essa escolha.
Essa camada de decisão fica sobre o laço PID, não dentro dele. O controlador nunca deve saber o que é um labirinto. Ele orienta o robô em direção a uma linha; a camada de interseções observa as mesmas leituras dos sensores em busca dos padrões que significam “algo mudou” e assume o controle quando um deles aparece.
Seis coisas que uma interseção pode ser
Toda interseção em um labirinto feito com fita se reduz a um de seis casos. Com um arranjo de reflexão de 8 canais lendo 1 para fita preta e 0 para a placa branca, cada caso produz uma assinatura:
- Virada à esquerda — os sensores externos do lado esquerdo ficam pretos enquanto o lado direito permanece branco.
- Virada à direita — a imagem espelhada.
- Interseção em T — a fita cobre toda a largura, e não há nada à frente.
- Cruzamento — a fita cobre toda a largura, e a linha continua à frente.
- Beco sem saída — todos os sensores leem branco; a linha simplesmente terminou.
- Chegada — um bloco sólido, largo e longo, que continua preto quando o robô entra nele.
A leitura sozinha é ambígua
Observe a interseção em T, o cruzamento e o marcador de chegada. Todos os três indicam 11111111. Um ramo à esquerda em que a linha também continua reta produz a mesma leitura que uma simples virada à esquerda.
Esta é a parte que vale internalizar: o padrão instantâneo do sensor não consegue classificar uma interseção. Um robô que decide no momento em que os sensores externos ficam pretos vai virar com confiança para a esquerda em um cruzamento no qual deveria ter seguido em frente.
A solução é o movimento, não mais sensores. Quando o arranjo indica pela primeira vez um padrão preto largo:
- Registre qual lado, ou quais lados, ficou preto — esse é o conjunto de ramos disponíveis.
- Continue avançando aproximadamente um comprimento do robô.
- Leia novamente. Se os sensores centrais ainda virem fita, a ida em frente também era uma opção. Se tudo estiver branco, era um T. Se tudo continuar ainda preto, o robô entrou no bloco de chegada.
Só agora você sabe que tipo de interseção deixou para trás, e só agora a camada do labirinto pode escolher o ramo.
Junction classify(Sensors& s) {
Branches at = s.readWideBranches(); // left / right seen on arrival
driveForward(ONE_ROBOT_LENGTH); // commit past the intersection
Reading after = s.read();
if (after.allBlack()) return FINISH; // still on tape = solid block
at.straight = after.centreOnLine(); // line continued through
return at.toJunction();
}
Observe que o classificador move o robô. Isso é incomum para uma função de aquisição de dados e é justamente o ponto — o tipo da interseção é uma propriedade de uma trajetória curta, não de uma única amostra.
Escolhendo um ramo
Depois que uma interseção é classificada, a camada do labirinto escolhe um ramo por uma regra fixa. A regra da mão esquerda — sempre pegar o ramo mais à esquerda disponível e retornar em um beco sem saída — é a escolha padrão porque não exige estado, é exaustiva em um labirinto sem ciclos e é trivial de verificar manualmente.
Consistência importa mais que esperteza aqui. Um robô que sempre prefere a esquerda eventualmente visitará todos os ramos alcançáveis, e o registro dessas curvas funciona como memória da rota que a etapa de simplificação depois reduz no caminho ótimo. Observe o mesmo comportamento de explorar e depois otimizar no Simulador de Labirintos — ele detecta paredes em vez de fita, mas a camada de decisão acima da aquisição de dados é idêntica.
O arranjo precisa ser mais largo que a linha
A detecção de interseções faz uma pergunta específica ao sensor: os canais mais externos ficaram pretos enquanto o centro ainda está sobre a linha? Um arranjo apenas um pouco mais largo que a fita não pode responder fisicamente.
left branch 8-channel array, ~60 mm wide
────────────────┐ [ 1 1 1 0 0 0 0 0 ] outer left black -> a branch exists
│
════════════════╪═══════ [ 0 0 0 1 1 0 0 0 ] centre still on the line
│
| Largura da fita | Largura mínima útil do arranjo | Por quê |
|---|---|---|
| 19 mm (padrão) | ~60 mm | Os sensores externos devem ultrapassar a lateral da fita por pelo menos um espaçamento completo entre sensores |
| 25 mm | ~75 mm | |
| 12 mm | ~40 mm |
A regra prática é três vezes a largura da fita. Se for mais estreito, o robô não conseguirá distinguir “a linha curvou muito” de “a linha se ramificou” — a decisão central sobre a qual todo o projeto se apoia. Um arranjo de 8 canais com espaçamento de 8 mm tem cerca de 60 mm, e por isso essa é a escolha padrão para fita de 19 mm.
Eliminando ruído da detecção de interseções
O arranjo atravessa uma interseção durante várias iterações do laço, e cada uma gera uma leitura nova. Um classificador que dispara no primeiro padrão largo muitas vezes disparará duas ou três vezes para uma única interseção, acrescentando à memória da rota caracteres que nunca corresponderam a curvas reais.
const uint8_t CONFIRM_SAMPLES = 3; // consecutive readings that must agree
const unsigned long JUNCTION_DEADTIME_MS = 300;
uint8_t agreeCount = 0;
uint8_t lastPattern = 0;
unsigned long lastJunctionAt = 0;
bool junctionDetected(uint8_t pattern) {
// Ignore everything for a short while after committing to one.
if (millis() - lastJunctionAt < JUNCTION_DEADTIME_MS) return false;
if (pattern == lastPattern) agreeCount++;
else { lastPattern = pattern; agreeCount = 1; }
if (agreeCount >= CONFIRM_SAMPLES && isWide(pattern)) {
lastJunctionAt = millis();
agreeCount = 0;
return true;
}
return false;
}
Dimensionar o tempo de inibição é um compromisso real e vale ser feito com números, não por sensação:
deadtime < (minimum spacing between junctions) / speed
A 0,2 m/s, com interseções nunca mais próximas que 15 cm, há 750 ms de folga — portanto 300 ms é seguro. A 0,5 m/s, em um labirinto com espaçamento de 10 cm, o orçamento cai para 200 ms, e é por isso que a exploração é lenta. Essa restrição, e não o algoritmo, é que define a velocidade de exploração.
Executando a curva
Classificar a interseção é metade do trabalho; executar a curva sem perder a linha é a outra metade, e uma curva de ângulo fixo é a maneira errada de fazer isso.
| Método | Como termina | Confiabilidade |
|---|---|---|
| Girar por um tempo fixo | Após N milissegundos | Ruim — muda com a tensão da bateria e a superfície |
| Girar um ângulo fixo com encoders | Após N contagens | Melhor, mas a folga e a patinação se acumulam |
| Girar até recuperar a linha | Quando os sensores centrais veem fita | Melhor — com autocorreção, termina onde a linha está |
void turnUntilLine(int direction) {
pivot(direction);
delay(120); // clear the junction's own tape first
while (!centreOnLine()) { // then rotate until the new line appears
if (millis() - startedAt > TURN_TIMEOUT_MS) { stopMotors(); fault(); return; }
}
stopMotors();
}
O delay(120) importa. Logo após iniciar a curva, os sensores centrais ainda estão sobre a fita da própria interseção; portanto, uma abordagem ingênua de “girar até ver a linha” para imediatamente, e o robô nem chega a virar. Saia da interseção primeiro e só depois procure a nova linha.
O timeout também importa: sem ele, um robô que vira além do ramo — porque estava rápido demais ou porque o ramo foi detectado incorretamente — gira para sempre.
A classificação completa, com a ida em frente resolvida
Juntando a eliminação de ruído, a passagem pela interseção e a curva:
Junction classifyAndTurn() {
const Branches at = readWideBranches(); // left and/or right, on arrival
driveForward(ONE_ROBOT_LENGTH); // commit past the intersection
const Reading after = read();
if (after.allBlack()) return FINISH; // still on tape = the solid block
Junction j;
j.left = at.left;
j.right = at.right;
j.straight = after.centreOnLine(); // resolved only by moving
if (!j.left && !j.right && !j.straight) return DEAD_END;
return j;
}
O classificador move o robô, o que é incomum para uma função de aquisição de dados e é todo o ponto: o tipo da interseção é uma propriedade de uma trajetória curta, não de uma única amostra. Três dos seis casos — T, cruzamento e chegada — são indistinguíveis no instante da chegada.
Testando sem um labirinto
Você não precisa de um labirinto para verificar esta camada, e construir um antes de a detecção funcionar é um desperdício comum de uma noite. Monte uma interseção de cada tipo em uma placa e passe o robô por cada uma, imprimindo a classificação:
| Peça de teste | Esperado | Falha comum |
|---|---|---|
| Linha reta simples | Nenhuma interseção | Dispara em uma curva suave — limiar externo sensível demais |
| Curva suave, raio de 15 cm | Nenhuma interseção | A mesma |
| Somente ramo à esquerda | LEFT |
Detectado como T — tempo de inibição curto demais; disparou duas vezes |
| Somente ramo à direita | RIGHT |
Espelho do caso anterior |
| Interseção em T | T |
Detectada como LEFT — não avançou além para resolver a ida em frente |
| Cruzamento | CROSS |
Detectado como T — distância de passagem curta demais |
| Beco sem saída | DEAD_END |
Detectado como perda de linha e o robô continuou |
| Bloco sólido de chegada | FINISH |
Detectado como CROSS — o bloco é maior que uma interseção |
O teste de curva suave é o que captura mais bugs, e é justamente o que ninguém pensa em executar. Uma curva de raio pequeno coloca momentaneamente vários sensores externos sobre a fita, com aparência idêntica à de um ramo. Se o seu robô inventa interseções em curvas, a solução é exigir que os sensores externos permaneçam pretos por uma distância mínima, e não apenas por uma contagem mínima de amostras — uma curva os libera rapidamente, mas um ramo real não.
Solução de problemas
| Sintoma | Causa provável | Solução |
|---|---|---|
| Interseções detectadas em curvas | Limiar externo sensível demais | Exigir que o padrão persista por uma distância mínima |
| Uma interseção registrada duas vezes | Não há tempo de inibição | 200–300 ms após confirmar uma interseção |
| T reportada como curva à esquerda | Não avançou além para resolver a ida em frente | O classificador deve se mover antes de decidir |
| Cruzamento reportado como T | Distância de passagem curta demais | Um comprimento completo do robô além da interseção |
| Chegada reportada como cruzamento | O bloco é maior que uma interseção | Continue avançando; ela continua preta, mas um cruzamento não |
| Interseções totalmente perdidas | Velocidade alta demais ou arranjo estreito demais | Reduza a velocidade de exploração; arranjo ≥ 3× a largura da fita |
| Curvas ultrapassam a nova linha | Curva por tempo ou ângulo fixo | Girar até os sensores centrais recuperarem a linha |
| Robô gira para sempre após uma interseção | Não há tempo-limite de giro | Adicione um e trate a falha de forma limpa |
| Nunca gira | Lê a fita da própria interseção como a nova linha | Saia da interseção antes de procurar |
| Funciona em uma placa, mas não em outra | Calibração ou luz ambiente | Recalibre a cada inicialização; subtração com emissor desligado |
Tornando isso confiável em fita real
Três coisas quebram a detecção de interseções no protótipo físico muito mais frequentemente do que o algoritmo:
- Deriva de calibração. Execute a varredura de calibração do arranjo sobre fita preta e placa branca sempre que ligar o robô e novamente ao se mudar para outra sala. A luz infravermelha ambiente do sol e das lâmpadas fluorescentes altera as leituras.
- Altura do sensor. 5–8 mm acima do piso. Alto demais, o contraste despenca; baixo demais, uma placa levemente ondulada encosta no arranjo.
- Qualidade da fita. Use fita de 18–20 mm e mantenha as interseções fisicamente limpas. Um canto desfiado parece um ramo intermitente, e nenhuma quantidade de código corrige uma fita que fornece leituras falsas.
Primeiro deixe o seguimento de linha simples estável em um oval — ajuste-o no Simulador de Seguidor de Linha e depois em fita real. Um labirinto esconderá problemas mecânicos e dos sensores atrás do que parece um bug de lógica.
Explore o grafo
Faz parte destas montagens
Projetos e caminhos de aprendizagem que incluem este tutorial.
Leitura complementar