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:
- 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:
- Anota qué lado (o lados) se pusieron negros: ese es el conjunto de ramas disponibles.
- Sigue avanzando aproximadamente una longitud de robot.
- 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