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.)

Diagrama de máquina de estados con cuatro estados: DRIVE (avanzar), SCAN (barrer con el servo), TURN (hacia la apertura) y REVERSE (retroceder). Las flechas muestran DRIVE hacia SCAN con «distancia por debajo del umbral de parada», SCAN hacia TURN con «lado libre encontrado», TURN de vuelta a DRIVE con «despejado, reanudar», y una rama REVERSE desde SCAN con «todos los lados bloqueados» que vuelve a barrer.
Cuatro estados y las condiciones entre ellos: avanzar hasta que algo esté cerca, barrer, girar hacia la apertura y retroceder solo cuando todas las direcciones están bloqueadas. Descargar SVG
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

Referencias