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:

Seis painéis mostrando tipos de interseção de labirinto de linhas desenhados como fita preta em uma placa clara, cada um sobreposto por uma barra de sensores de oito canais: curva apenas à esquerda lê 11111000, curva apenas à direita lê 00011111, interseção T lê todos os uns, cruzamento lê todos os uns, beco sem saída lê todos os zeros e o marcador de chegada é um bloco sólido que lê todos os uns.
Os seis tipos de interseção e o padrão que cada um produz em um arranjo de 8 canais. Três deles são indistinguíveis no instante da chegada. Baixar SVG
  • 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:

  1. Registre qual lado, ou quais lados, ficou preto — esse é o conjunto de ramos disponíveis.
  2. Continue avançando aproximadamente um comprimento do robô.
  3. 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

Referências