Tutorial · Principiante · 22 min de lectura
Algoritmo de evitación de obstáculos: medir, escanear, girar
El bucle de decisión de un robot esquivador: umbral de frenado, barrido con servomotor y una máquina de estados que busca espacio libre sin atascarse.
Una vez que un robot puede leer una distancia limpia y buscar la dirección libre, queda una pieza: la lógica que decide qué hacer. Este es el cerebro de un robot que evita obstáculos—un bucle corto que avanza, nota que algo está demasiado cerca, mira alrededor y gira hacia el espacio libre. Es más simple de lo que suena, y acertar con él consiste sobre todo en no quedarse atascado.
Reactivo, no planificado
Este robot no tiene mapa. Reacciona a lo que tiene delante ahora mismo: percibe, decide, actúa, repite. Eso es navegación reactiva, y es el primer enfoque correcto—barato, robusto, y suficiente para recorrer una habitación desordenada sin golpearse. No es lo mismo que la planificación de rutas, donde un robot usa un mapa para elegir un recorrido con antelación (eso es territorio de A* sobre una rejilla de ocupación). La evitación reactiva es la base; la planificación viene después.
El umbral de parada
Todo pivota en un número: ¿a qué distancia está demasiado cerca? Elige una distancia a la que el robot se detenga y lo vuelva a pensar. Si es demasiado pequeña, roza los obstáculos antes de poder reaccionar; si es demasiado grande, se para ante todo y avanza nerviosamente. El valor correcto depende de la velocidad—un robot más rápido tiene que reaccionar antes, porque recorre más terreno antes de poder parar. Para un robot pequeño a un ritmo tranquilo, 15–25 cm es un punto de partida razonable.
La máquina de estados
Un sensor de distancia es lo que evita la mayoría de los obstáculos; un final de carrera es lo que atrapa los que no puede ver, y todo robot que funcione sin supervisión quiere ambos.
La forma más limpia de expresar el comportamiento es una máquina de estados finita: unos cuantos estados con nombre y reglas claras para pasar de unos a otros. Sustituye un enredo de if anidados por algo que puedes leer y depurar. (Para el patrón general, mira máquinas de estados para el comportamiento de un robot.)
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;
}
}
Elegir el sentido de giro
El barrido ya hizo la parte difícil: devuelve el rumbo más abierto. Gira hacia ahí. La llamada turnToward(angle) traduce «hay espacio a la izquierda» a un pivote a la izquierda y «hay espacio a la derecha» a un pivote a la derecha, usando tracción diferencial—una rueda delante y la otra atrás, para girar más o menos sobre el sitio.
Evitar la oscilación y los callejones sin salida
Dos modos de fallo atrapan a los robots ingenuos, y ambos tienen arreglos sencillos:
- Cambiar de opinión entre izquierda y derecha en una esquina donde ambas se parecen. Añade una duración mínima de giro para que el robot se comprometa con un giro en vez de volver a decidir en cada bucle.
- Encajarse en un callejón sin salida donde todas las direcciones están bloqueadas. Para eso está el estado REVERSE: retroceder y volver a barrer. Un pequeño contador de atascos—retroceder con más fuerza si has barrido varias veces sin progreso—rompe las trampas peores.
Ajuste: velocidad frente a distancia de reacción
Dos perillas deciden si el robot parece suave o nervioso: la velocidad de avance y el umbral de parada. Sube la velocidad y tendrás que subir el umbral para mantener segura la distancia de frenado — v·t para la latencia y v²/2a para el frenado—, y por eso doblar la velocidad más que dobla el umbral. La forma más rápida de encontrar una buena pareja es cambiarlas donde un error no cueste nada—el simulador de evitación de obstáculos— y solo entonces pasar los valores al hardware.
Histéresis: dos umbrales, no uno
La máquina de estados de arriba usa un único STOP_CM, y eso es lo primero que hay que arreglar. Un robot
que ronda un umbral alterna entre DRIVE y SCAN muchas veces por segundo, porque cambios diminutos en la
medida dan la vuelta a la comparación. Se arrastra en vez de comprometerse.
const int STOP_CM = 20; // stop when closer than this
const int RESUME_CM = 30; // do not drive again until clearer than this
La distancia entre ellos es histéresis, y fuerza al comportamiento a comprometerse. El mismo patrón arregla casi toda oscilación por umbral en el código de un robot, no solo esta — un detector de flancos, un aviso de batería baja, un acercamiento a base.
Una regla útil para el tamaño de la distancia: hazla mayor que tu ruido de medición más la distancia que el robot recorre en un bucle. Para un robot a 0,3 m/s con un ciclo de sensor de 60 ms, eso son 18 mm de recorrido más un par de centímetros de dispersión ultrasónica — así que una distancia de 10 cm es generosa y una de 3 cm todavía vibrará.
Comprometerse con un giro
La segunda oscilación está en el nivel de la decisión: el robot gira a la izquierda, el barrido ahora muestra otra cosa, gira a la derecha, y se pone a titubear sobre el sitio. Dos mecanismos lo arreglan:
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;
Una duración mínima de giro evita que el robot reevalúe antes de que el giro haya cambiado nada, y compara con el umbral de reanudación en vez de con el de parada.
Detectar que estás atascado
REVERSE resuelve un callejón sin salida delante. No resuelve el caso más difícil: un robot que
técnicamente se mueve, barre y gira, y no llega a ninguna parte — una esquina cóncava, o una silla
rodeada una y otra vez.
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();
}
}
Hay dos puntos de diseño aquí que merecen nombre propio.
Escalamiento. La primera respuesta a un obstáculo es un giro pequeño; la tercera respuesta a la misma situación es uno grande. Una respuesta fija repetida es lo que mantiene a un robot en una esquina.
Aleatoriedad. Un robot determinista en una trampa simétrica toma la misma decisión cada vez y entra en bucle para siempre. Un componente aleatorio en el giro de escape rompe la simetría, y es la razón por la que los primeros robots aspiradores usaban rebote aleatorio en vez de una regla ingeniosa — es realmente robusto frente a trampas que no anticipaste.
Leer los finales de carrera en todos los estados
La máquina de estados tal como está escrita solo comprueba distancia, y todo sensor de distancia tiene superficies que no puede ver. Un robot sin detección de contacto empuja una cortina hasta que los motores se atascan.
El contacto debería manejarse por encima de la máquina de estados, no dentro de un 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 ... */ }
}
Fíjate en el signo: gira en dirección contraria al final de carrera que se disparó. Esto es exactamente la razón por la que dos finales de carrera se ganan su sitio frente a uno — un único interruptor central te dice que has golpeado algo, y deja el sentido de giro en manos del azar.
Velocidad y umbral, en tabla
El umbral de parada no es una constante; es una función de la velocidad, porque la distancia de frenado crece con el cuadrado de la velocidad. Para un robot con 80 ms de latencia de detección y decisión, frenando a 1,0 m/s², con un radio de 90 mm y un margen de 50 mm:
| Velocidad | Término de latencia | Término de frenado | Umbral 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 |
Doblar la velocidad de 0,3 a 0,6 m/s no dobla el umbral — pero sí lo hace crecer un 76 %, y a 0,9 m/s el robot tiene que reaccionar a más de medio metro. Por eso un robot que evita de forma fiable a paso de caminante empieza a golpearse cuando lo aceleras, y por eso el arreglo es un umbral dependiente de la velocidad en vez de un único valor fijo grande que vuelve tímido al robot por todas partes.
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 el estado es la herramienta de depuración
La línea más útil que puedes añadir:
if (state != lastReportedState) {
Serial.print(millis()); Serial.print(F(" -> "));
Serial.println(STATE_NAMES[state]);
lastReportedState = state;
}
Imprimir al cambiar en vez de en cada bucle mantiene la salida legible, y convierte un robot que
«a veces se comporta de forma extraña» en una transcripción que puedes leer. Casi todos los bugs de este
comportamiento se ven como una secuencia de estados que no tiene sentido — un TURN que no acaba
nunca, un SCAN que se dispara cuarenta veces por segundo, un REVERSE que vuelve a entrar
inmediatamente.
Si no puedes responder a «¿qué está haciendo el robot ahora mismo» mirando una sola variable, la máquina de estados no está haciendo su trabajo.
Resolución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
| Se arrastra ante los obstáculos | Un umbral, sin histéresis | Para a 20 cm, reanuda a 30 cm |
| Titubea izquierda-derecha sobre el sitio | Vuelve a decidir a mitad de giro | Duración mínima de giro |
| Gira en bucle eternamente en la misma esquina | Escape determinista | Escala y añade un componente aleatorio |
| Se mete en cortinas y cojines | El sonido se absorbe — física | Finales de carrera, manejados por encima de la máquina de estados |
| Choca al retroceder | Sin detección trasera | Un final de carrera trasero, o limita el tiempo de retroceso |
| Evita bien despacio, golpea a alta velocidad | Umbral fijo, frenado cuadrático | Umbral dependiente de la velocidad |
| Se congela a intervalos aleatorios | Lecturas de sensor bloqueantes | Ping y barrido no bloqueantes |
| Retrocede justo después de retroceder | REVERSE reentra antes de que el barrido se actualice |
Fuerza un barrido nuevo después de retroceder |
| Gira hacia la pared contra la que acaba de golpear | Gira hacia el final de carrera que se disparó | Gira en dirección contraria |
| No puedes saber qué está haciendo | No se imprime el estado | Imprime al cambiar de estado |
Explora el grafo
Forma parte de estos montajes
Proyectos y rutas de aprendizaje que incluyen este tutorial.
Lecturas adicionales