Tutorial · Iniciante · 20 min
Ler um sensor ultrassônico HC-SR04: distância confiável em código
Como ler um HC-SR04 no Arduino: use pulseIn com tempo limite, converta o tempo do eco em centímetros e aplique um filtro de mediana.
Um robô que evita obstáculos é tão bom quanto a distância usada para orientá-lo. Esse número vem de um sensor ultrassônico HC-SR04: ele emite um pulso, escuta o eco, e o tempo de ida e volta informa a distância até o objeto mais próximo. Este guia transforma a leitura bruta em uma distância em centímetros confiável — uma que sobreviva às falhas do sensor e aos ecos espúrios, em vez de mandar o robô contra uma parede.
Como o sensor informa a distância
O HC-SR04 fala uma linguagem simples de dois pinos. Você coloca Trigger em HIGH por 10 microssegundos; ele emite oito rajadas de 40 kHz e mantém Echo em nível alto até o reflexo retornar. O tempo em que Echo permanece em HIGH é o tempo de ida e volta, e o som viaja a aproximadamente 343 m/s, portanto:
distance_cm = echo_time_us * 0.0343 / 2
A divisão por dois existe porque o som foi emitido e retornou. A página do componente aborda a ligação e a física; aqui, importa fazer uma leitura limpa em código.
Leia com pulseIn — e sempre defina um tempo limite
O tempo limite importa por um segundo motivo além da correção: pulseIn bloqueia, portanto um eco perdido interrompe todo o ciclo pelo tempo que você permitir. Consulte temporização sem bloqueio para saber o que isso custa a um robô em movimento.
pulseIn mede quanto tempo um pino permanece em HIGH. A armadilha: se o eco nunca retornar, seja porque nada está ao alcance ou porque uma superfície macia dispersa o som, pulseIn bloqueará até o tempo limite padrão e seu ciclo de controle parará. Sempre passe um tempo limite explícito, dimensionado para seu alcance máximo:
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);
}
Retornar -1 para um tempo limite esgotado — em vez de 0 — importa. Um 0 indica “um obstáculo está tocando no sensor”, o oposto de “o caminho está livre”. Mantenha “sem eco” diferente de “muito perto”.
Por que a leitura bruta varia
Aponte um HC-SR04 para uma parede e as leituras ainda oscilarão, porque o mundo real é bagunçado:
- Alvos macios ou inclinados dispersam o eco para longe do receptor, produzindo às vezes um valor enorme ou uma perda de leitura.
- O feixe de ~15° faz o sensor informar o objeto mais próximo em um cone, portanto a perna de uma mesa ao lado pode gerar uma leitura curta inesperada.
- Consultas muito rápidas (com menos de ~60 ms de intervalo) deixam o eco do pulso anterior vazar para a leitura seguinte.
Se você alimentar essas variações brutas diretamente em uma decisão de parar ou virar, o robô se moverá aos trancos. Você precisa suavizar o fluxo sem adicionar atraso.
Um filtro de mediana de 5
A ferramenta certa é uma mediana, não uma média. Uma média espalha um único pico extremo por várias leituras; uma mediana simplesmente o descarta. Faça cinco medições e use a do meio:
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
}
Dois detalhes tornam isso robusto: mapear um tempo limite esgotado para um grande valor “distante” (para que um eco perdido nunca pareça um obstáculo) e inserir um pequeno delay entre as medições, evitando interferência entre ecos. Para a teoria sobre filtragem sem suavização excessiva, consulte ruído, viés e filtragem de sensores.
Não bloqueie o ciclo
Tudo acima ainda interrompe o ciclo. pulseIn espera, delay(30) espera, e cinco medições com
30 ms de intervalo consomem 150 ms durante os quais o robô não lê nenhum outro sensor e não toma nenhuma decisão. A
0,3 m/s, isso equivale a 4,5 cm de deslocamento sem leitura por medição.
A solução é controlar o sensor por uma pequena máquina de estados que o ciclo principal percorre sem nunca esperar:
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;
}
}
O ciclo agora chama updatePing() e segue em frente. lastDistance sempre é a medição
concluída mais recente, com idade máxima de 60 ms, e nada mais no robô esperou por ela.
Cronometre o eco com uma interrupção
A máquina de estados ainda precisa saber quando o eco chegou, e consultar digitalRead no
ciclo principal reduz a resolução. Uma interrupção nas duas bordas do pino Echo não custa nada:
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);
}
Em um Arduino Uno, isso deve usar o pino 2 ou 3 — os únicos dois pinos de interrupção externa — e, se seu robô tiver encoders de roda, eles também precisam desses dois pinos. O conflito é real e está entre as causas mais comuns para um projeto migrar para um Mega ou um ESP32, ou passar a usar no Nano as interrupções por mudança de pino.
Observe a proteção echoStart != 0. Sem ela, uma borda de descida que chega sem uma borda de subida anterior
— o que ocorre na primeira interrupção depois de um tempo limite esgotado — calcula uma duração sem sentido a partir
de um carimbo de tempo antigo.
Escolhendo um filtro
A mediana é a opção padrão, mas vale saber o que as alternativas realmente fazem, porque elas falham de maneiras diferentes:
| Filtro | Rejeita um único pico? | Atraso | Custo | Use quando |
|---|---|---|---|---|
| Nenhum | Não | 0 | — | Nunca, em um robô em movimento |
| Média de 5 | Não — espalha o pico por 5 amostras | 2 amostras | Baixo | O ruído é gaussiano, não em picos |
| Mediana de 5 | Completamente | 2 amostras | Ordenação, trivial com n=5 | Ultrassônico. Esta é a resposta certa |
| Mediana de 3 | Sim, um pico | 1 amostra | O menor | Você precisa de mais responsividade |
| Exponencial (EMA) | Não | Depende de α | O menor | Suavizar um sinal que já está limpo |
| Mediana e depois EMA | Sim, além da suavização | 2 amostras + α | Baixo | Ciclos rápidos que precisam dos dois |
O ruído ultrassônico não é gaussiano — são valores atípicos ocasionais e extremos em um sinal de outro modo estável, precisamente o caso que uma média trata mal e uma mediana trata perfeitamente. Um pico de 400 cm em um conjunto de cinco leituras de 30 cm desloca a média em 74 cm e a mediana em nada.
Temperatura, se você precisar de precisão
O divisor 58,2 pressupõe 343 m/s, que é a velocidade do som a 20 °C. Ela não é constante:
speed (m/s) = 331.3 + 0.606 x temperature_celsius
| Temperatura | Erro em relação à suposição de 20 °C | A 2 m |
|---|---|---|
| 0 °C | Lê 3,5% longe demais | 7 cm |
| 10 °C | 1,8% longe demais | 3,5 cm |
| 30 °C | 1,7% perto demais | 3,4 cm |
| 40 °C | 3,5% perto demais | 7 cm |
Para desviar de obstáculos, desconsidere isso completamente — 7 cm a 2 m não alteram nenhuma decisão. Para um robô que mede uma abertura, ou que funciona em uma garagem no inverno, compense:
float cmPerMicrosecond(float tempC) {
return (331.3f + 0.606f * tempC) / 20000.0f; // /1e6 for us, /2 for round trip
}
Se o robô já tiver um MPU-6050 — ou praticamente qualquer sensor I²C — você terá uma leitura de temperatura disponível sem custo adicional.
Usando vários sensores
Dois HC-SR04 que emitam pulsos simultaneamente ouvirão os pulsos um do outro, produzindo leituras erráticas em ambos. Eles devem operar alternadamente:
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);
}
}
O custo é a taxa de atualização: três sensores com 60 ms cada significa que cada sensor individual é atualizado a cada 180 ms. Se isso for lento demais — e em um robô em movimento geralmente é — a resposta é usar menos sensores ultrassônicos ou combinar com um VL53L0X, que usa luz e, portanto, não interfere de forma alguma com o sonar.
Solução de problemas
| Sintoma | Causa provável | Correção |
|---|---|---|
| O robô congela aleatoriamente | pulseIn sem tempo limite |
Passe um tempo limite ou use a máquina de estados sem bloqueio |
| Paradas de emergência em uma sala vazia | Tratar um tempo limite esgotado como 0 cm | Tempo limite esgotado significa ausência de eco, não distância válida. Retorne um valor sentinela e trate-o explicitamente |
| As leituras alternam de forma errática | Consultas com menos de ~60 ms | Espere o eco anterior desaparecer |
| Valores constantes, mas alguns por cento errados | Temperatura | Compense com a fórmula acima |
| Leituras erráticas com dois sensores | Eles ouvem um ao outro | Use revezamento, nunca operação simultânea |
| Funciona na bancada, mas é ruidoso no robô | Ruído dos motores no trilho de 5 V | Desacople a alimentação do sensor; afaste seus fios dos fios dos motores |
| Um valor enorme ocasional sobrevive ao filtro | Média em vez de mediana | Use a mediana — uma média espalha o pico, não o rejeita |
| O pino do ESP32 parou de responder | Entrada Echo de 5 V em GPIO de 3,3 V | Use um divisor em Echo; verifique se o pino ainda funciona |
Próximos passos
Agora você tem um único valor confiável: a distância à frente. Ele pode ser usado de duas formas. Monte o sensor em um servo e varra-o para ver qual direção está livre — varra um sensor ultrassônico com um servo — e forneça o resultado ao ciclo de decisão do algoritmo de desvio de obstáculos. Você pode testar todo o comportamento de percepção e direção primeiro no simulador de desvio de obstáculos.
Explore o grafo
Faz parte destas montagens
Projetos e caminhos de aprendizagem que incluem este tutorial.
Leitura complementar