Tutorial · Intermedio · 14 min de lectura

Detección de cruces en laberintos de línea con una matriz IR

Cómo distingue un seguidor de línea una bifurcación en T de una cruz, un callejón sin salida de la meta, y por qué una sola lectura no basta para decidir.

Un seguidor de línea tiene una tarea: mantener la línea centrada. Un robot de laberinto de líneas tiene dos, y la segunda es donde se atascan los principiantes. Debe seguir siguiendo la línea, pero también debe detectar los momentos en que la línea ofrece una elección, y decidir qué tipo de elección es.

Esa capa de decisión se apoya sobre el lazo PID, no vive dentro de él. El controlador no debería saber nunca qué es un laberinto. Gira hacia una línea; la capa de cruces observa las mismas lecturas de sensores buscando los patrones que significan “algo ha cambiado” y toma el control cuando aparece uno.

Seis cosas que puede ser un cruce

Todo cruce en un laberinto de líneas de cinta se reduce a uno de seis casos. Con una matriz de reflectancia de 8 canales que lee 1 para la cinta negra y 0 para el tablero blanco, cada uno produce una firma:

Seis paneles que muestran tipos de cruce de laberinto de líneas dibujados como cinta negra sobre un tablero claro, cada uno superpuesto con una barra de ocho sensores: solo giro a la izquierda lee 11111000, solo giro a la derecha lee 00011111, la bifurcación en T lee todo unos, la cruz lee todo unos, el callejón sin salida lee todo ceros y el marcador de meta es un bloque sólido que lee todo unos.
Los seis tipos de cruce y el patrón que cada uno deja en una matriz de 8 canales. Tres de ellos son indistinguibles en el momento de llegar. Descargar SVG
  • Solo giro a la izquierda: los sensores exteriores izquierdos se ponen negros mientras el lado derecho sigue blanco.
  • Solo giro a la derecha: la imagen especular.
  • Bifurcación en T: la cinta cruza toda la anchura y no hay nada por delante.
  • Cruz: la cinta cruza toda la anchura y la línea continúa hacia delante.
  • Callejón sin salida: todos los sensores leen blanco; la línea simplemente terminó.
  • Meta: un bloque sólido relleno, ancho y largo, que sigue negro mientras avanzas sobre él.

La lectura por sí sola es ambigua

Mira la bifurcación en T, la cruz y el marcador de meta. Los tres reportan 11111111. Una rama a la izquierda en la que la línea también continúa recta reporta lo mismo que un giro a la izquierda simple.

Esto es lo que merece interiorizarse: el patrón instantáneo del sensor no puede clasificar un cruce. Un robot que decide en el momento en que sus sensores exteriores se ponen negros girará a la izquierda con toda confianza en un cruce que debería haber atravesado recto.

La solución es movimiento, no más sensores. Cuando la matriz reporta por primera vez un patrón negro ancho:

  1. Anota qué lado (o lados) se pusieron negros: ese es el conjunto de ramas disponibles.
  2. Sigue avanzando aproximadamente una longitud de robot.
  3. Vuelve a leer. Si los sensores centrales aún ven cinta, el recto también era una opción. Si todo está blanco, era una T. Si todo sigue todavía negro, has entrado en el bloque de meta.

Solo ahora sabes qué tipo de cruce has dejado atrás, y solo ahora puede elegir la capa de laberinto.

Junction classify(Sensors& s) {
  Branches at = s.readWideBranches();      // izquierda / derecha vistos al llegar
  driveForward(ONE_ROBOT_LENGTH);          // comprometerse mas alla de la interseccion
  Reading after = s.read();

  if (after.allBlack())  return FINISH;    // aun sobre la cinta = bloque solido
  at.straight = after.centreOnLine();      // la linea continuo a traves
  return at.toJunction();
}

Fíjate en que el clasificador conduce. Es inusual para una función de sensado y es justamente el punto: el tipo de cruce es una propiedad de una trayectoria corta, no de una sola muestra.

Elegir una rama

Una vez clasificado el cruce, la capa de laberinto elige una rama con una regla fija. La regla de la mano izquierda (tomar siempre la rama disponible más a la izquierda y volver atrás en un callejón sin salida) es la elección estándar porque no tiene estado, es exhaustiva en un laberinto sin bucles y se verifica a mano con trivialidad.

Aquí la consistencia importa más que la astucia. Un robot que siempre prefiere la izquierda acabará visitando todas las ramas alcanzables, y el registro de sus giros es lo que el paso de simplificación de ruta compactará después hasta la ruta óptima. Observa ese mismo comportamiento de explorar y luego optimizar en el simulador de resolución de laberintos: ese detecta paredes en lugar de cinta, pero la capa de decisión por encima del sensado es idéntica.

La matriz tiene que ser más ancha que la línea

La detección de cruces le plantea una pregunta concreta al sensor: ¿se han puesto negros los canales más exteriores mientras el centro sigue sobre la línea? Una matriz apenas más ancha que la cinta es físicamente incapaz de responderla.

   rama izquierda                  matriz de 8 canales, ~60 mm de ancho
   ────────────────┐          [ 1 1 1 0 0 0 0 0 ]   exterior izq. negro -> existe una rama
                   │
   ════════════════╪═══════   [ 0 0 0 1 1 0 0 0 ]   el centro sigue sobre la linea
                   │
Anchura de cinta Anchura mínima útil de la matriz Por qué
19 mm (estándar) ~60 mm Los sensores exteriores deben rebasar la cinta al menos una separación entre sensores
25 mm ~75 mm
12 mm ~40 mm

La regla práctica es tres veces la anchura de la cinta. Más estrecha y el robot no puede distinguir “la línea se curvó bruscamente” de “la línea se bifurcó”, que es la decisión central sobre la que descansa todo el proyecto. Una matriz de 8 canales con 8 mm de separación da unos 60 mm, y por eso es la elección estándar para cinta de 19 mm.

Antirrebote en un cruce

La matriz cruza un cruce a lo largo de varias iteraciones del bucle, y cada una es una lectura nueva. Un clasificador que dispara con el primer patrón ancho disparará a menudo dos o tres veces por un solo cruce, llenando la ruta grabada con caracteres que nunca fueron giros reales.

const uint8_t CONFIRM_SAMPLES = 3;      // lecturas consecutivas que deben coincidir
const unsigned long JUNCTION_DEADTIME_MS = 300;

uint8_t agreeCount = 0;
uint8_t lastPattern = 0;
unsigned long lastJunctionAt = 0;

bool junctionDetected(uint8_t pattern) {
  // Ignora todo durante un rato despues de comprometerse con uno.
  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 el tiempo muerto es un compromiso real, y merece la pena hacerlo con números en lugar de por intuición:

tiempo muerto < (separacion minima entre cruces) / velocidad

A 0,2 m/s con cruces no más cerca de 15 cm, eso da 750 ms de margen, así que 300 ms es seguro. Circula a 0,5 m/s en un laberinto con 10 cm de separación y el presupuesto baja a 200 ms, y por eso las pasadas de exploración son lentas. Esta restricción, y no el algoritmo, es lo que fija la velocidad de exploración.

El giro en sí

Clasificar el cruce es la mitad del trabajo; ejecutar el giro sin perder la línea es la otra mitad, y un giro de ángulo fijo es la forma equivocada de hacerlo.

Método Cómo termina Fiabilidad
Girar un tiempo fijo Tras N milisegundos Mala: cambia con la tensión de la batería y la superficie
Girar un ángulo fijo según codificadores Tras N cuentas Mejor, pero el juego mecánico y el deslizamiento se acumulan
Girar hasta reencontrar la línea Cuando los sensores centrales ven cinta La mejor: se autocorrige y termina donde está la línea
void turnUntilLine(int direction) {
  pivot(direction);
  delay(120);                        // despejar primero la cinta del propio cruce
  while (!centreOnLine()) {          // luego girar hasta que aparezca la nueva linea
    if (millis() - startedAt > TURN_TIMEOUT_MS) { stopMotors(); fault(); return; }
  }
  stopMotors();
}

El delay(120) importa. Justo después de iniciar el giro los sensores centrales siguen sobre la cinta del propio cruce, así que un “gira hasta ver la línea” ingenuo se detiene al instante y el robot no gira nunca. Despeja el cruce primero y luego mira.

El tiempo de espera también importa: sin él, un robot que se pasa de la rama (porque iba demasiado rápido o porque la rama se detectó mal) gira para siempre.

La clasificación completa, con el recto resuelto

Juntando el antirrebote, el avance más allá del cruce y el giro:

Junction classifyAndTurn() {
  const Branches at = readWideBranches();     // izquierda y/o derecha, al llegar

  driveForward(ONE_ROBOT_LENGTH);             // comprometerse mas alla de la interseccion
  const Reading after = read();

  if (after.allBlack()) return FINISH;        // aun sobre la cinta = el bloque solido

  Junction j;
  j.left     = at.left;
  j.right    = at.right;
  j.straight = after.centreOnLine();          // solo se resuelve moviendose

  if (!j.left && !j.right && !j.straight) return DEAD_END;
  return j;
}

El clasificador conduce, cosa inusual en una función de sensado y que es justo el punto: el tipo de cruce es una propiedad de una trayectoria corta, no de una sola muestra. Tres de los seis casos (T, cruz y meta) son indistinguibles en el instante de llegar.

Probarlo sin laberinto

No necesitas un laberinto para verificar esta capa, y construir uno antes de que la detección funcione es una pérdida de tarde habitual. Coloca un cruce de cada tipo sobre un tablero y pasa el robot por cada uno, imprimiendo la clasificación:

Pieza de prueba Esperado Fallo habitual
Línea recta simple Ningún cruce Se dispara en una curva suave: umbral exterior demasiado sensible
Curva suave, radio de 15 cm Ningún cruce Igual
Solo rama izquierda LEFT Detectado como T: tiempo muerto demasiado corto, disparó dos veces
Solo rama derecha RIGHT Imagen especular del anterior
Bifurcación en T T Detectado como LEFT: no avanzó para resolver el recto
Cruz CROSS Detectado como T: la distancia de avance fue demasiado corta
Callejón sin salida DEAD_END Se detectó como pérdida de línea y el robot siguió
Bloque sólido de meta FINISH Detectado como CROSS: el bloque es más largo que un cruce

La prueba de la curva suave es la que más fallos atrapa, y es la que nadie piensa en ejecutar. Una curva cerrada pone momentáneamente varios sensores exteriores sobre la cinta, lo que se parece exactamente a una rama. Si tu robot inventa cruces en las curvas, la solución es exigir que los sensores exteriores se mantengan negros a lo largo de una distancia mínima, no solo de un número mínimo de muestras: una curva los libera enseguida, una rama real no.

Solución de problemas

Síntoma Causa probable Solución
Se detectan cruces en las curvas Umbral exterior demasiado sensible Exige que el patrón persista a lo largo de una distancia mínima
Un cruce se registra dos veces Sin tiempo muerto 200-300 ms tras comprometerse con un cruce
Se reporta T como giro a la izquierda No avanzó para resolver el recto El clasificador debe moverse antes de decidir
Se reporta cruz como T Distancia de avance demasiado corta Una longitud de robot completa más allá de la intersección
Se reporta meta como cruz El bloque es más largo que un cruce Sigue conduciendo: permanece negro, una cruz no
No se detecta ningún cruce Demasiada velocidad o matriz demasiado estrecha Reduce la velocidad de exploración; matriz >= 3x la anchura de cinta
Los giros se pasan de la nueva línea Giro de tiempo o ángulo fijo Gira hasta que los sensores centrales reenganchen
El robot gira para siempre tras un cruce Sin tiempo de espera de giro Añádelo y declara el fallo limpiamente
Nunca gira Lee la cinta del propio cruce como la nueva línea Despeja el cruce antes de buscar
Funciona en un tablero y no en otro Calibración o luz ambiente Recalibra en cada encendido; resta con emisor apagado

Conseguir fiabilidad con cinta real

Hay tres cosas que rompen la detección de cruces en el hardware con mucha más frecuencia que el algoritmo:

  • Deriva de calibración. Ejecuta el barrido de calibración de la matriz sobre cinta negra y tablero blanco cada vez que enciendas, y vuelve a ejecutarlo cuando cambies de sala. El infrarrojo ambiente de la luz solar y de las lámparas fluorescentes cambia las lecturas.
  • Altura del sensor. De 5 a 8 mm por encima del suelo. Demasiado alto y el contraste se derrumba; demasiado bajo y un tablero ligeramente combado roza la matriz.
  • Calidad de la cinta. Usa cinta de 18-20 mm y haz los cruces físicamente limpios. Una esquina deshilachada se lee como una rama intermitente, y ningún código arregla una cinta que te miente.

Consigue primero un seguimiento de línea estable en un óvalo sencillo: sintonízalo en el simulador de seguidor de línea y luego sobre cinta real. Un laberinto ocultará problemas mecánicos y de sensores detrás de lo que parece un fallo de lógica.

Explora el grafo

Forma parte de estos montajes

Proyectos y rutas de aprendizaje que incluyen este tutorial.

Lecturas adicionales

Referencias