Tutorial · Principiante · 22 min

Algoritmo di evitamento ostacoli: rileva, scandisci, decidi, gira

Scopri il ciclo di un robot che evita ostacoli: soglia di arresto, scansione con servomotore e macchina a stati che gira verso lo spazio libero senza bloccarsi.

Quando un robot può leggere una distanza affidabile e cercare la direzione libera, rimane un pezzo: la logica che decide che cosa fare. È il cervello di un robot che evita ostacoli—un breve ciclo che avanza, nota che qualcosa è troppo vicino, guarda intorno e gira verso lo spazio libero. È più semplice di quanto sembri e farlo bene dipende soprattutto dal non bloccarsi.

Reattivo, non pianificato

Questo robot non ha una mappa. Reagisce a ciò che ha davanti in quel momento: rileva, decide, agisce e ripete. Questa è la navigazione reattiva, il primo approccio corretto—economico, robusto e sufficiente a esplorare una stanza affollata senza colpire nulla. Non è la stessa cosa della pianificazione del percorso, in cui il robot usa una mappa per scegliere una rotta in anticipo (il territorio di A* su una griglia di occupazione). L’evitamento reattivo è la base; la pianificazione viene dopo.

La soglia di arresto

Tutto dipende da un numero: quanto vicino è troppo vicino? Scegli la distanza alla quale il robot si ferma e rivaluta. Se è troppo piccola, urta gli ostacoli prima di poter reagire; se è troppo grande, si ferma davanti a tutto e avanza timidamente. Il valore corretto dipende dalla velocità: un robot più veloce deve reagire prima, perché percorre più terreno prima di potersi fermare. Per un piccolo robot che avanza con moderazione, 15–25 cm è un punto di partenza sensato.

La macchina a stati

Un sensore di distanza evita la maggior parte degli ostacoli; un interruttore a urto intercetta quelli che non vede, e ogni robot destinato a funzionare senza supervisione vuole entrambi.

Il modo più pulito di esprimere il comportamento è una macchina a stati finiti: pochi stati con nome e regole chiare per passare da uno all’altro. Sostituisce un intrico di if annidati con qualcosa che puoi leggere e correggere. (Per il modello generale, vedi macchine a stati finiti per il comportamento del robot.)

Diagramma di una macchina a stati con quattro stati: DRIVE (avanzare), SCAN (eseguire la scansione con il servomotore), TURN (girare verso l'apertura) e REVERSE (indietreggiare). Le frecce mostrano il passaggio da DRIVE a SCAN quando «la distanza è inferiore alla soglia di arresto», da SCAN a TURN quando «è stato trovato un lato libero», da TURN a DRIVE quando «lo spazio è libero, riprendi», e un ramo REVERSE da SCAN quando «tutti i lati sono bloccati», che torna alla nuova scansione.
Quattro stati e le condizioni fra essi: avanza finché qualcosa si avvicina, scandisci, gira verso l'apertura e indietreggia soltanto quando ogni direzione è bloccata. Scarica 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;
  }
}

Scelta della direzione di rotazione

La scansione ha già fatto la parte difficile: restituisce la direzione più libera. Gira da quel lato. La chiamata turnToward(angle) trasforma «libero a sinistra» in una rotazione a sinistra e «libero a destra» in una rotazione a destra, usando la trazione differenziale: una ruota avanti e l’altra indietro per ruotare più o meno sul posto.

Evita oscillazioni e vicoli ciechi

Due modalità di guasto intrappolano i robot ingenui, e entrambe hanno correzioni semplici:

  • Oscillazione fra sinistra e destra in un angolo in cui entrambe le direzioni sembrano simili. Aggiungi una durata minima della rotazione, così il robot mantiene l’impegno nella svolta invece di riconsiderare a ogni ciclo.
  • Incastro in un vicolo cieco, dove ogni direzione è bloccata. A questo serve lo stato REVERSE: indietreggia, poi esegui una nuova scansione. Un piccolo contatore di blocco—indietreggia con più forza se hai scandito più volte senza progressi—rompe le trappole peggiori.

Regolazione: velocità e distanza di reazione

Due parametri decidono se il robot sembra fluido o agitato: velocità di marcia e soglia di arresto. Se aumenti la velocità, devi aumentare anche la soglia per mantenere sicura la distanza di frenatura — v·t per la latenza e v²/2a per la frenata — ed è per questo che raddoppiare la velocità aumenta la soglia di più del doppio. Il modo più rapido per trovare una buona coppia è modificarla dove un errore non costa nulla — nel simulatore di evitamento ostacoli — e solo dopo trasferire i valori all’hardware.

Isteresi: due soglie, non una

La macchina a stati precedente usa un solo STOP_CM, ed è la prima cosa da correggere. Un robot che si aggira vicino a una soglia passa da DRIVE a SCAN molte volte al secondo, perché variazioni minime della misura invertono il confronto. Si ferma a scatti invece di mantenere un comportamento deciso.

const int STOP_CM   = 20;   // stop when closer than this
const int RESUME_CM = 30;   // do not drive again until clearer than this

La distanza fra le due è isteresi e costringe il comportamento a essere deciso. Lo stesso modello risolve quasi ogni oscillazione basata su soglie nel codice di un robot, non solo questa: un rilevatore di bordi, un avviso di batteria scarica, un avvicinamento alla stazione di ricarica.

Una regola utile per dimensionare la distanza: deve superare il rumore della misura più lo spostamento del robot durante un ciclo. Per un robot a 0.3 m/s con un ciclo del sensore di 60 ms, sono 18 mm di spostamento più un paio di centimetri di dispersione degli ultrasuoni: una distanza di 10 cm è generosa, mentre una di 3 cm può ancora vibrare.

Mantieni l’impegno nella svolta

La seconda oscillazione si verifica a livello decisionale: il robot gira a sinistra, la scansione mostra ora qualcosa di diverso, gira a destra e oscilla sul posto. Due meccanismi lo risolvono:

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 durata minima della rotazione impedisce al robot di rivalutare prima che la svolta abbia cambiato qualcosa, e il confronto usa la soglia di ripresa, non quella di arresto.

Rileva che il robot è bloccato

REVERSE gestisce un vicolo cieco davanti. Non gestisce il caso più difficile: un robot che tecnicamente si muove, scandisce e gira senza arrivare da nessuna parte — un angolo concavo o una sedia attorno alla quale continua a girare.

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();
  }
}

Vale la pena nominare due aspetti progettuali.

Escalation. La prima risposta a un ostacolo è una piccola svolta; la terza risposta alla stessa situazione è una svolta grande. Ripetere una risposta fissa è ciò che intrappola un robot in un angolo.

Casualità. Un robot deterministico in una trappola simmetrica sceglie sempre la stessa opzione e ripete il ciclo per sempre. Una componente casuale nella svolta di fuga rompe la simmetria; è il motivo per cui i primi robot aspiratori usavano rimbalzi casuali anziché una regola elaborata, una soluzione realmente robusta anche contro trappole impreviste.

Leggi gli interruttori a urto in ogni stato

La macchina a stati così scritta controlla soltanto la distanza, mentre ogni sensore di distanza ha superfici che non vede. Un robot senza rilevamento del contatto spinge una tenda finché i motori non si bloccano.

Il contatto deve essere gestito al di sopra della macchina a stati, non dentro uno stato:

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 ... */ }
}

Osserva il segno: gira dall’altra parte rispetto all’interruttore che è scattato. Ecco perché due interruttori si guadagnano il loro posto rispetto a uno: un singolo interruttore centrale comunica che hai colpito qualcosa, ma lascia la direzione della svolta al caso.

Velocità e soglia, in forma di tabella

La soglia di arresto non è una costante: è una funzione della velocità, perché la distanza di frenatura cresce con il quadrato della velocità. Per un robot con 80 ms di latenza fra sensazione e decisione, frenata a 1.0 m/s², raggio di 90 mm e margine di 50 mm:

Velocità Termine di latenza Termine di frenatura Soglia totale
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

Raddoppiare la velocità da 0.3 a 0.6 m/s non raddoppia la soglia, ma la aumenta del 76%; a 0.9 m/s il robot deve reagire a oltre mezzo metro di distanza. Ecco perché un robot che evita gli ostacoli in modo attendibile a passo d’uomo comincia a sbattere contro le cose quando aumenti la velocità e perché la correzione è una soglia dipendente dalla velocità, non un unico valore fisso elevato che rende timido il robot ovunque.

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);
}

Stampare lo stato è lo strumento di diagnostica

La singola riga più utile che puoi aggiungere:

if (state != lastReportedState) {
  Serial.print(millis()); Serial.print(F(" -> "));
  Serial.println(STATE_NAMES[state]);
  lastReportedState = state;
}

Stampare a ogni cambiamento, anziché a ogni ciclo, mantiene leggibile l’uscita e trasforma un robot che «a volte si comporta in modo strano» in una trascrizione che puoi analizzare. Quasi ogni bug di questo comportamento è visibile come una sequenza di stati priva di senso: un TURN che non finisce mai, uno SCAN che scatta quaranta volte al secondo, un REVERSE che vi rientra immediatamente.

Se non riesci a rispondere alla domanda «che cosa sta facendo il robot adesso?» guardando una sola variabile, la macchina a stati non sta svolgendo il suo compito.

Risoluzione dei problemi

Sintomo Causa probabile Correzione
Si ferma a scatti davanti agli ostacoli Una sola soglia, senza isteresi Fermati a 20 cm e riprendi a 30 cm
Oscilla fra sinistra e destra sul posto Riconsidera la decisione durante la svolta Durata minima della rotazione
Gira per sempre nello stesso angolo Fuga deterministica Aumenta progressivamente l’impegno e aggiungi una componente casuale
Spinge tende e cuscini Il suono viene assorbito: fisica Interruttori a urto, gestiti al di sopra della macchina a stati
Indietreggia contro gli oggetti Nessun sensore posteriore Un interruttore a urto posteriore, oppure limita il tempo di retromarcia
Evita bene lentamente, ma colpisce ad alta velocità Soglia fissa e frenatura quadratica Soglia dipendente dalla velocità
Si blocca a intervalli casuali Letture bloccanti del sensore Ping e scansione non bloccanti
Inverte la marcia subito dopo averla invertita REVERSE è stato rientrato prima che la scansione si aggiornasse Forza una nuova scansione dopo la retromarcia
Gira verso il muro appena colpito Gira verso il paraurti che è scattato Gira dall’altra parte
Non riesci a capire che cosa sta facendo Non stampa lo stato Stampa al cambio di stato

Esplora il grafo

Fa parte di queste costruzioni

Progetti e percorsi di apprendimento che includono questo tutorial.

Per approfondire

Fonti