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:

Gráfico de linhas com leituras de distância de um HC-SR04 enquanto um robô se aproxima de uma parede, de cerca de 58 cm até 15 cm. O sinal bruto (cinza, com marcadores) tem um pico de até cerca de 85 cm rotulado como 'eco espúrio rejeitado' e duas perdas de leitura próximas de zero rotuladas como 'tempo limite esgotado rejeitado', enquanto a linha da mediana de 5 (ciano) permanece suave diante de ambos.
Uma única passagem da mediana de 5 rejeita tanto o eco distante espúrio quanto as perdas por tempo limite, deixando uma distância suave sobre a qual o controlador pode agir. Baixar SVG
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

Referências