Tutorial · Principiante · 20 min
Leggere un HC-SR04: distanza affidabile nel codice
Come leggere un HC-SR04 su Arduino: pulseIn con timeout, conversione del tempo dell'eco in centimetri e filtro mediano che elimina i picchi casuali.
Un robot che evita ostacoli è tanto efficace quanto la distanza su cui governa. Questo dato proviene da un sensore ultrasonico HC-SR04: emette un segnale, ascolta l’eco e il tempo di percorrenza di andata e ritorno indica quanto è lontano l’oggetto più vicino. Questa guida trasforma il segnale grezzo in una distanza in centimetri attendibile, resistente alle interruzioni del sensore e agli echi indesiderati, che altrimenti potrebbero mandare il robot contro un muro.
Come il sensore comunica la distanza
HC-SR04 parla un semplice linguaggio a due pin. Porti Trigger a HIGH per 10 microsecondi: il sensore emette otto impulsi da 40 kHz e mantiene Echo alto finché l’eco non torna. Il tempo per cui Echo resta HIGH è il tempo di percorrenza di andata e ritorno e, poiché il suono viaggia a circa 343 m/s, ottieni:
distance_cm = echo_time_us * 0.0343 / 2
La divisione per due dipende dal fatto che il suono ha percorso sia l’andata sia il ritorno. La pagina del componente spiega il cablaggio e la fisica; qui ci concentriamo su come leggerlo in modo pulito nel codice.
Leggilo con pulseIn e imposta sempre un timeout
Il timeout è importante anche per un secondo motivo oltre alla correttezza: pulseIn blocca, quindi un eco mancante ferma l’intero loop per l’intervallo consentito. Consulta la temporizzazione non bloccante per capire quanto ciò costi a un robot in movimento.
pulseIn misura per quanto tempo un pin resta HIGH. Il problema è questo: se l’eco non torna mai, perché non c’è nulla nel raggio d’azione o perché una superficie morbida disperde il suono, pulseIn resta bloccato per l’intero timeout predefinito e il loop di controllo si arresta. Passa sempre un timeout esplicito, dimensionato sulla portata massima:
const int TRIG = 9, ECHO = 10;
const unsigned long TIMEOUT_US = 25000UL; // ~4 m of round trip
long pingCm() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, TIMEOUT_US);
if (us == 0) return -1; // timed out: nothing in range
return (long)(us * 0.0343 / 2.0);
}
Restituire -1 per un timeout anziché 0 è importante. Un valore 0 indica «un ostacolo sta toccando il sensore», l’esatto contrario di «la via è libera». Devi mantenere distinte le condizioni «nessun eco» e «molto vicino».
Perché la lettura grezza oscilla
Se punti un HC-SR04 verso un muro, le letture oscillano comunque, perché il mondo reale è complesso:
- Bersagli morbidi o inclinati disperdono l’eco lontano dal ricevitore, quindi occasionalmente ottieni un valore enorme o un’interruzione.
- Il fascio di ~15° fa sì che il sensore riporti l’oggetto più vicino in un cono, perciò una gamba laterale di un tavolo può far salire di colpo una lettura breve.
- Eseguire le interrogazioni troppo rapidamente (a meno di ~60 ms di distanza) permette all’eco del ping precedente di contaminare la lettura successiva.
Se passi questi improvvisi valori grezzi direttamente a una decisione di arresto o svolta, il robot si muove a scatti. Devi uniformare il flusso senza aggiungere ritardo.
Un filtro mediano su 5 valori
Lo strumento giusto è la mediana, non la media. Una media distribuisce un singolo picco anomalo su più letture; la mediana lo scarta semplicemente. Effettua cinque ping e usa quello centrale:
int cmpLong(const void* a, const void* b) {
return (*(long*)a) - (*(long*)b);
}
long medianCm() {
long s[5];
for (int i = 0; i < 5; i++) {
long v = pingCm();
s[i] = (v < 0) ? 400 : v; // treat timeout as "far", not "touching"
delay(30); // let the previous echo die away
}
qsort(s, 5, sizeof(long), cmpLong);
return s[2]; // the middle value
}
Due dettagli rendono questa soluzione robusta: mappare un timeout su un valore elevato di «lontano», così un eco perso non viene mai letto come ostacolo, e inserire un piccolo delay tra i ping, così gli echi non si sovrappongono. Per la teoria sul filtraggio senza eccessiva uniformazione, consulta rumore dei sensori, bias e filtraggio.
Smetti di bloccare il loop
Tutto quanto sopra ferma ancora il loop. pulseIn attende, delay(30) attende e cinque ping distanti 30 ms richiedono 150 ms nei quali il robot non legge altri sensori e non prende decisioni. A 0.3 m/s sono 4.5 cm di percorso alla cieca per ogni misurazione.
La soluzione è comandare il sensore tramite una piccola macchina a stati che il loop principale attraversa senza mai attendere:
enum PingState { IDLE, TRIGGERED, WAITING };
PingState state = IDLE;
unsigned long stateSince = 0;
long lastDistance = -1;
const unsigned long PING_PERIOD_MS = 60; // let the previous echo die away
const unsigned long ECHO_TIMEOUT_MS = 30; // beyond the sensor's range
void updatePing() {
const unsigned long now = millis();
switch (state) {
case IDLE:
if (now - stateSince < PING_PERIOD_MS) return;
digitalWrite(TRIG, HIGH);
stateSince = micros(); // note: microseconds for this short step
state = TRIGGERED;
break;
case TRIGGERED:
if (micros() - stateSince < 10) return;
digitalWrite(TRIG, LOW);
stateSince = now;
state = WAITING;
break;
case WAITING:
if (echoComplete) { // set by the interrupt below
lastDistance = echoMicros / 58.2;
echoComplete = false;
stateSince = now;
state = IDLE;
} else if (now - stateSince > ECHO_TIMEOUT_MS) {
lastDistance = -1; // no echo — far, or invisible
stateSince = now;
state = IDLE;
}
break;
}
}
Ora il loop chiama updatePing() e prosegue. lastDistance è sempre la misurazione completata più recente, risalente al massimo a 60 ms, e nient’altro nel robot ha atteso per ottenerla.
Misurare l’eco con un interrupt
La macchina a stati deve ancora sapere quando è arrivato l’eco e interrogare digitalRead nel loop principale costa risoluzione. Un interrupt su entrambi i fronti del pin Echo non costa nulla:
volatile unsigned long echoStart = 0;
volatile unsigned long echoMicros = 0;
volatile bool echoComplete = false;
void echoISR() {
if (digitalRead(ECHO) == HIGH) {
echoStart = micros();
} else if (echoStart != 0) {
echoMicros = micros() - echoStart;
echoStart = 0;
echoComplete = true;
}
}
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
attachInterrupt(digitalPinToInterrupt(ECHO), echoISR, CHANGE);
}
Su un Arduino Uno deve essere il pin 2 o 3, gli unici due pin con interrupt esterni, e se il robot ha encoder delle ruote li vuole entrambi. Il conflitto è reale ed è uno dei motivi più comuni per cui un progetto passa agli interrupt su cambio di pin di un Nano, a un Mega o a un ESP32.
Nota la protezione echoStart != 0. Senza di essa, un fronte di discesa senza un precedente fronte di salita, cosa che accade al primo interrupt dopo un timeout, calcolerebbe una durata priva di senso partendo da una marca temporale obsoleta.
Scegliere un filtro
La mediana è la scelta predefinita corretta, ma vale la pena sapere cosa fanno davvero le alternative, perché falliscono in modi diversi:
| Filtro | Respinge un singolo picco? | Ritardo | Costo | Quando usarlo |
|---|---|---|---|---|
| Nessuno | No | 0 | — | Mai, su un robot in movimento |
| Media di 5 | No: distribuisce il picco su 5 campioni | 2 campioni | Basso | Il rumore è gaussiano, non a picchi |
| Mediana di 5 | Completamente | 2 campioni | Ordinamento, trascurabile con n=5 | Ultrasuoni. È la risposta giusta |
| Mediana di 3 | Sì, un picco | 1 campione | Il più basso | Ti serve una reattività maggiore |
| Esponenziale (EMA) | No | Dipende da α | Il più basso | Uniformare un segnale già pulito |
| Mediana e poi EMA | Sì, più uniformazione | 2 campioni + α | Basso | Loop veloci che richiedono entrambe |
Il rumore ultrasonico non è gaussiano: si tratta di valori anomali estremi occasionali su un segnale altrimenti stabile, ossia proprio il caso in cui una media gestisce male il segnale e una mediana lo gestisce in modo perfetto. Un picco di 400 cm in un insieme di cinque letture da 30 cm sposta la media di 74 cm e la mediana di nulla.
Temperatura, se ti serve la precisione
Il divisore 58.2 presuppone 343 m/s, la velocità del suono a 20 °C. Non è costante:
speed (m/s) = 331.3 + 0.606 x temperature_celsius
| Temperatura | Errore rispetto all’ipotesi di 20 °C | A 2 m |
|---|---|---|
| 0 °C | Legge il 3.5% troppo lontano | 7 cm |
| 10 °C | 1.8% troppo lontano | 3.5 cm |
| 30 °C | 1.7% troppo vicino | 3.4 cm |
| 40 °C | 3.5% troppo vicino | 7 cm |
Per evitare ostacoli, ignora del tutto questo aspetto: 7 cm a 2 m non cambiano alcuna decisione. Per un robot che misura uno spazio, o che lavora in un garage d’inverno, compensa:
float cmPerMicrosecond(float tempC) {
return (331.3f + 0.606f * tempC) / 20000.0f; // /1e6 for us, /2 for round trip
}
Se il robot monta già un MPU-6050, o praticamente qualsiasi sensore I²C, hai a disposizione una lettura della temperatura senza alcun costo aggiuntivo.
Utilizzare più sensori
Due HC-SR04 che emettono ping contemporaneamente sentiranno entrambi il segnale dell’altro, producendo letture estreme su entrambi. Devono alternarsi:
const int SENSORS = 3;
int current = 0;
long distance[SENSORS];
void updateAll() {
if (pingComplete(current)) {
distance[current] = lastDistance;
current = (current + 1) % SENSORS; // round-robin
startPing(current);
}
}
Il costo è la frequenza di aggiornamento: tre sensori a 60 ms ciascuno significano che ogni singolo sensore si aggiorna ogni 180 ms. Se è troppo lento, cosa che spesso accade su un robot in movimento, la risposta è usare meno sensori ultrasonici oppure aggiungere un VL53L0X, che usa la luce e quindi non può interferire in alcun modo con il sonar.
Risoluzione dei problemi
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Il robot si blocca in modo casuale | pulseIn senza timeout |
Passa un timeout oppure usa la macchina a stati non bloccante |
| Arresti di emergenza in una stanza vuota | Il timeout viene trattato come 0 cm | Un timeout significa lontano. Restituisci un valore sentinella e gestiscilo |
| Le letture oscillano in modo estremo | Interrogazioni più rapide di ~60 ms | Lascia estinguersi l’eco precedente |
| Valori stabili ma errati di qualche percentuale | Temperatura | Compensa con la formula precedente |
| Letture estreme con due sensori | Si sentono a vicenda | Alternanza circolare, mai simultanea |
| Funziona sul banco, ma è rumoroso sul robot | Rumore dei motori sulla linea dei 5 V | Disaccoppia l’alimentazione del sensore; tieni i suoi cavi lontani dai cavi dei motori |
| Un valore enorme sporadico supera il filtro | Media anziché mediana | Usa la mediana: la media distribuisce il picco, non lo respinge |
| Un pin dell’ESP32 ha smesso di rispondere | Eco a 5 V su un GPIO a 3.3 V | Dividitore di tensione su Echo; verifica che il pin funzioni ancora |
Come proseguire
Ora hai un unico dato pulito: la distanza frontale. Da qui puoi fare altri due passaggi. Monta il sensore su un servomotore ed effettua una scansione per vedere in che direzione c’è spazio, con una scansione servo di un sensore ultrasonico, e alimenta il risultato al loop decisionale dell’algoritmo di evitamento ostacoli. Prima puoi provare l’intero comportamento di rilevamento e sterzo nel simulatore di evitamento ostacoli.
Esplora il grafo
Fa parte di queste costruzioni
Progetti e percorsi di apprendimento che includono questo tutorial.
Per approfondire