PTENES
MODULE 1.4 · PRACTICE

📡 Sensors: how the robot perceives

The “sensing” part of the robot cycle. You’ll learn the four ways a sensor can deliver data, measure distance with the HC-SR04 without risking the ESP32, detect lines and obstacles with infrared, filter out noise with an averaging filter, connect sensors to the I2C bus and read everything at once, without blocking the program.

6
Topics
Beginner
Level
Practice
Type
HC-SR04 + IR
Hardware (or Wokwi)
0%
0 of 0 topics read · Section 1 of 6

Detailed content

1

🗣️ How a sensor “speaks” to the board

Before connecting any sensor, find out what “language” it uses to deliver its data. There are four: digital (yes or no, like “there’s an obstacle”), analog (a voltage that varies, like the ground’s reflectance), pulse width (the data is in the length of a pulse, as with the HC-SR04) and bus (the sensor sends ready-made numbers using a protocol, such as I2C). The language determines the pin, the reading function, and the electrical precautions.

🌱 New here?

A sensor module is a sensor already assembled on a small board with resistors and, sometimes, a comparator that converts analog into digital. The data sheet (datasheet) is the manufacturer’s document with the operating voltage, pins, and timings. A bus is a pair of wires shared by several devices, each with its own address.

digitaldigitalRead() analoganalogRead() pulse width pulseIn() duration = valueI2C bus Wireaddress + data, bit by bit

How to read this map: each panel shows a language, with the Arduino function that understands it in the corner. At the top (green), the value is in the voltage level. At the bottom (cyan), the value is in TIME: in the pulse duration or the sequence of bits. The ones below require the program to wait or communicate, so they come up again in topic 6.

Sensor in the kitLanguagePowerCaution
HC-SR04pulse width5 VECHO returns 5 V: use a divider
Obstacle IRdigital3,3 Vadjust the trimpot
TCRT5000 (line)analog (AO) or digital (DO)3,3 Vuse an ADC1 pin
VL53L0X, MPU-6050I2C3.3 V (module)different addresses

Why learn this

Once you know the language, any new sensor leads to a quick set of questions: which pin, which function, which voltage? You no longer have to rely on a tutorial for every component; you can read the data sheet.

Key concepts

Digital signal
yes or no
ADC
voltage becomes a number
Pulse
duration is the data
I2C
two wires, addresses
2

🦇 HC-SR04: distance by echo

O HC-SR04 It works like a bat. You send a short pulse to the TRIG; it emits an ultrasonic chirp (40 kHz, which you can't hear) and keeps the pin ECHO high until the echo returns. The pulse duration is the round-trip time. Since sound travels at about 343 m/s (0.0343 cm per microsecond), the distance is duration × 0.0343 ÷ 2, divided by 2 because the sound traveled there and back.

HC-SR04 obstacleecho returnsTRIG 10 µsECHO duration = outbound + return ESP32GPIO 5 (TRIG)GPIO 18 (ECHO) 1 kΩ 2 kΩGND

How to read this map: at the top, the sound path to the obstacle and back. In the middle, the two signals over time: the short TRIG pulse and the longer ECHO pulse, whose width you measure. On the right, the protection: ECHO outputs 5 V and passes through the 1 kΩ/2 kΩ divider, which supplies about 3.3 V to GPIO 18. TRIG can connect directly: the ESP32's 3.3 V is enough to trigger it.

Three-step setup

1

Power

VCC to 5 V (the DevKit's VIN/5V pin, powered by USB) and GND to common GND. The HC-SR04P version accepts 3.3 V and doesn't need the divider; check which version you have.

2

TRIG direct

TRIG to GPIO 5 with a simple jumper wire.

3

ECHO through the divider

ECHO → 1 kΩ → midpoint → GPIO 18; connect 2 kΩ from the midpoint to GND. Module 1.2 calculation: 5 × 2 ÷ 3 ≈ 3.3 V. The divider isn't needed in Wokwi, but build it for practice.

Goal: measure the distance five times per second and display it in centimeters, with a message when there is no echo. In Wokwi, add "HC-SR04" and click it during the simulation to change the distance.

hc-sr04.ino
// HC-SR04: distance by echo (ESP32, esp32 3.x package)
const int PINO_TRIG = 5;
const int PINO_ECHO = 18;            // through the 1k/2k divider on real hardware
const unsigned long TIMEOUT_US = 30000;  // ~5 m: no echo after this

float medirCm() {
  digitalWrite(PINO_TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH); delayMicroseconds(10);  // trigger pulse
  digitalWrite(PINO_TRIG, LOW);
  unsigned long dur = pulseIn(PINO_ECHO, HIGH, TIMEOUT_US);
  if (dur == 0) return -1;              // no echo
  return dur * 0.0343 / 2.0;             // cm (round trip)
}

void setup() {
  Serial.begin(115200);
  pinMode(PINO_TRIG, OUTPUT);
  pinMode(PINO_ECHO, INPUT);
  Serial.println("HC-SR04 pronto");
}

void loop() {
  float cm = medirCm();
  if (cm < 0) Serial.println("no echo (too far away or soft surface)");
  else { Serial.print("distance: "); Serial.print(cm, 1); Serial.println(" cm"); }
  delay(200);
}

How to check

  • ✓Hold your hand about 20 cm from the sensor: the Monitor should show something close to "distance: 20.0 cm"; check with a ruler (an error of 1 to 2 cm is normal).
  • ✓In Wokwi, the value follows the sensor's slider control.
  • ✗Always seeing "no echo"? Check that VCC is connected to 5 V and that TRIG and ECHO aren't swapped.

✓ The HC-SR04 works well with

  • ✓A wall, box, or chair leg directly in front of it.
  • ✓Distances from about 2 cm to 2 to 3 m indoors.
  • ✓Hard, smooth surfaces.

✗ The HC-SR04 gets confused by

  • ✗Curtains, sofas, cushions: they absorb sound.
  • ✗A wall at a steep angle: the echo bounces off to the side.
  • ✗Two HC-SR04s triggering at the same time: one picks up the other's echo.

Why learn this

It's the rover's "eye" for avoiding obstacles in module 1.6. Knowing its limits (soft surfaces, angles) explains why the robot sometimes bumps into a curtain but not a wall.

Key concepts

TRIG / ECHO
triggers / measures
Speed of sound
0.0343 cm/µs
pulseIn
measures the pulse, with timeout
Divider on ECHO
5 V → 3,3 V
3

🔦 Infrared: line and obstacle

Infrared (IR) reflection sensors have an LED that emits invisible light and a receiver next to it. Light surfaces reflect a lot; black surfaces absorb it. With a line sensor (TCRT5000), pointed at the ground a few millimeters away, this distinguishes black tape from the floor. With an obstacle sensor, pointed forward, a nearby object reflects light back and the module signals it. Many modules include a comparator with a trimpot (small blue screw) that converts the analog signal to digital.

🌱 New here?

O threshold is the value that separates "line" from "floor": above it is one, below it is the other. Calibrating means measuring the value on light and dark surfaces in the real environment, then setting the threshold halfway between them. On many modules, the digital DO output goes LOW when it detects something; check the indicator LED on the board.

IR LED · receiver light floor reflects a lotreading ≈ 300 IR LED · receiver black tapeabsorbs reading ≈ 3500 black thresholdin the middlelight

How to read this map: the red arrows show infrared light. On the left, it all reflects back; in the middle, almost none returns. On common TCRT5000 modules, less light at the receiver = a HIGHER ADC reading (300 and 3500 are examples; your values will be different). On the right, the scale: set the threshold halfway between the readings for light and black, measured on your floor.

Goal: calibrate the line sensor (AO on GPIO 35) during the first 5 seconds by moving it over light and black surfaces, then display "LINE" or "floor"; also read an obstacle sensor (DO on GPIO 27). In Wokwi, use a potentiometer in place of AO and a button in place of DO.

ir-linha-obstaculo.ino
// Analog line IR with calibration + digital obstacle IR
const int PINO_LINHA = 35;       // TCRT5000 AO, ADC1
const int PINO_OBST  = 27;       // obstacle module DO
int minimo = 4095, maximo = 0, limiar = 2048;

void setup() {
  Serial.begin(115200);
  pinMode(PINO_OBST, INPUT_PULLUP); // hold the level if the module is disconnected
  Serial.println("Calibrating for 5 s: move the sensor over light and black surfaces");
  unsigned long inicio = millis();
  while (millis() - inicio < 5000) {
    int v = analogRead(PINO_LINHA);
    if (v < minimo) minimo = v;
    if (v > maximo) maximo = v;
    delay(5);
  }
  limiar = (minimo + maximo) / 2;
  Serial.print("min="); Serial.print(minimo);
  Serial.print(" max="); Serial.print(maximo);
  Serial.print(" limiar="); Serial.println(limiar);
}

void loop() {
  int v = analogRead(PINO_LINHA);
  bool naLinha = (v > limiar);                    // black = high value
  bool obstaculo = (digitalRead(PINO_OBST) == LOW);  // many modules: LOW = detected
  Serial.print("linha="); Serial.print(v);
  Serial.print(naLinha ? " LINHA" : " chao");
  Serial.println(obstaculo ? " | OBSTACULO" : " | livre");
  delay(100);
}

How to check

  • ✓After calibration, "max" should be much higher than "min" (a difference of hundreds or more).
  • ✓Over the tape, it should show "LINE"; off the tape, "floor". Hold your hand a few cm from the obstacle sensor: "OBSTACLE".
  • ✗Are min and max almost the same? The sensor is too high (ideally, it's about 3 to 10 mm above the floor), or you didn't move it over the black surface.

Why learn this

These are the kit's cheapest sensors and the foundation of the line follower in Track 2, where five of them side by side form an array. The automatic calibration you just wrote is the same one used in project 2.6.

Key concepts

Reflection
light reflects, black absorbs
Threshold
boundary between two states
Calibration
measure real light and dark surfaces
Trimpot
adjusts the digital output
4

📉 Noise and averaging filter

No sensor gives the same number twice. The reading "jumps" because of noise electrical noise, vibration, lost echo. Sometimes an absurd value appears (0 cm, 900 cm) and, if the robot takes it seriously, it brakes for no reason. Two simple tools solve almost everything: discard anything outside the possible range and calculate a moving average of the last N valid readings.

🌱 New here?

A moving average stores the last N readings in a circular list and returns their average; with each new reading, the oldest one is removed. The median sorts the values and takes the middle one, which works well against isolated spikes. The cost of filtering is delay: with a large N, the curve is smooth but takes longer to react.

discarded spike lost echoraw (noisy)filtered time → (the object moved closer midway through the graph)

How to read this map: the cyan line shows what the sensor outputs; the green line shows what the robot should use. The red circles are impossible readings, discarded before averaging. Notice that when the object moves closer, the green line drops a little after the cyan one: this is the filter's delay, small with 5 samples.

Goal: apply rejection + a 5-reading moving average to the HC-SR04 from topic 2 and view both curves together in the Serial Plotter.

filtro-media.ino
// Reject invalid readings + moving average on the HC-SR04 (ESP32, esp32 core 3.x)
const int PINO_TRIG = 5, PINO_ECHO = 18;
const int N = 5;                    // window size
float janela[N];
int pos = 0, cheias = 0;

float medirCm() {
  digitalWrite(PINO_TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(PINO_TRIG, LOW);
  unsigned long dur = pulseIn(PINO_ECHO, HIGH, 30000);
  return (dur == 0) ? -1 : dur * 0.0343 / 2.0;
}

float filtrar(float novo) {
  janela[pos] = novo; pos = (pos + 1) % N;  // circular list
  if (cheias < N) cheias++;
  float soma = 0;
  for (int i = 0; i < cheias; i++) soma += janela[i];
  return soma / cheias;
}

void setup() {
  Serial.begin(115200);
  pinMode(PINO_TRIG, OUTPUT); pinMode(PINO_ECHO, INPUT);
}

void loop() {
  float cru = medirCm();
  static float filtrado = 0;
  if (cru >= 2 && cru <= 400) filtrado = filtrar(cru);  // discard impossible values
  Serial.print("cru:"); Serial.print(cru);
  Serial.print(",filtrado:"); Serial.println(filtrado);
  delay(60);                         // HC-SR04 needs about 60 ms between measurements
}

How to check: in the Serial Plotter, "raw" has teeth and spikes (including -1 when there is no echo), while "filtered" is a smooth line that follows your hand with a slight delay. Change N to 15 and see the delay increase.

✓ Filtering done right

  • ✓Discard first, then average.
  • ✓Small window (3 to 7) for quick decisions.
  • ✓Use the median when spikes are rare and huge.

✗ Filtering that gets in the way

  • ✗Average of 50 readings: the robot sees the wall too late.
  • ✗Include the "no echo" value -1 in the average.
  • ✗Filter out noise in code when it comes from a loose wire.

Why learn this

A robot that reacts to noise brakes, turns, and hesitates for no reason. With the filter, the decision in module 1.6 becomes reliable. In Track 2, the same idea comes up again as a complementary filter for the IMU and a filter on the PID derivative.

Key concepts

Moving average
last N readings
Median
the middle value
Out of range
discard first
Delay
the cost of smoothness
5

🔗 I2C sensors: two wires, multiple sensors

O I2C is a two-wire bus, SDA (data) and SCL (clock), shared by multiple devices. Each has a 7-bit address written in hexadecimal: the VL53L0X laser distance sensor usually responds at 0x29, the MPU-6050 IMU at 0x68, and the small OLED display at 0x3C. On the classic ESP32, the default pins are SDA = GPIO 21 e SCL = GPIO 22, and the library is Wire, which is already included in the package.

ESP32GPIO 21 SDA GPIO 22 SCL SDASCL pull-ups → 3.3 VVL53L0X0x29 MPU-60500x68 OLED0x3C

How to read this map: the two horizontal lines are the entire bus. Each cyan module connects to them with two wires (white dots) and responds only when the ESP32 calls its address. The amber resistors keep the lines at 3.3 V when no one is communicating; most modules already have these pull-ups on the board. In addition to SDA and SCL, each module needs 3.3 V and GND.

From zero to your first reading

1

Connect four wires

VCC → 3.3 V, GND → GND, SDA → GPIO 21, SCL → GPIO 22.

2

Run the scanner

It checks all 127 addresses to see which ones respond. If your sensor appears, the wiring is correct.

3

Install the sensor library

Tools → Manage Libraries. For the VL53L0X, search for "VL53L0X" and install the Pololu library. Open one of its examples under File → Examples to check the version.

Goal: list all connected I2C devices in the Serial Monitor. In Wokwi, add an "MPU6050" connected to 21/22, and the scanner should find 0x68.

i2c-scanner.ino
// I2C scanner: what's on the bus? (ESP32, esp32 core 3.x)
#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin();                       // SDA 21, SCL 22 on the classic ESP32
  delay(300);
  Serial.println("Searching for I2C devices...");
  int achados = 0;
  for (byte endereco = 1; endereco < 127; endereco++) {
    Wire.beginTransmission(endereco);
    if (Wire.endTransmission() == 0) { // 0 = a device responded
      Serial.print("  found at 0x");
      if (endereco < 16) Serial.print("0");
      Serial.println(endereco, HEX);
      achados++;
    }
  }
  Serial.print(achados); Serial.println(" dispositivo(s).");
}

void loop() {}

How to check: with a VL53L0X, you'll see "found at 0x29" and "1 device(s)."; with the Wokwi MPU-6050, you'll see 0x68. Zero devices means SDA and SCL are swapped, GND is missing, or the module isn't powered.

Goal: read the distance in millimeters from the VL53L0X (physical board; Wokwi doesn't have this part) using Pololu's VL53L0X library.

vl53l0x.ino
// VL53L0X laser distance over I2C (Pololu VL53L0X library)
#include <Wire.h>
#include <VL53L0X.h>

VL53L0X sensor;

void setup() {
  Serial.begin(115200);
  Wire.begin();
  sensor.setTimeout(500);
  if (!sensor.init()) {
    Serial.println("VL53L0X not found: check wires and address");
    while (true) delay(1000);
  }
  sensor.startContinuous();           // measure continuously
}

void loop() {
  int mm = sensor.readRangeContinuousMillimeters();
  if (sensor.timeoutOccurred()) Serial.println("timeout");
  else { Serial.print("distance: "); Serial.print(mm); Serial.println(" mm"); }
  delay(100);
}

How to check: with your hand at 10 cm, you'll see something close to "distance: 100 mm"; with nothing in front (beyond about 1.2 m), values close to 8190 indicate "out of range".

Why learn this

IMUs, displays, color sensors, pressure sensors, and laser distance sensors use I2C. You can connect several of them using two pins, which will come in handy in Track 2, when the robot gets an IMU and encoders and pins start running out.

Key concepts

SDA / SCL
data / clock
Address
0x29, 0x68, 0x3C
Wire library
Arduino I2C
Scanner
checks the wiring
6

⏱️ Read multiple sensors without blocking

A delay(500) seems harmless, but during that half-second the ESP32 does nothing else: it doesn't read the IR sensor, detect the wall, or stop the motor. The solution is to replace the wait with a question: "has enough time passed since the last time?", using millis(). Each sensor gets its own clock, and the loop() runs thousands of times per second, handling whichever task is due.

🌱 New here?

Code blocking is code that stops everything while it waits (like delay). Non-blocking checks the clock and moves on. A periodic task is a piece of code that runs every X milliseconds. The calculation millis() - ultimo >= intervalo, with unsigned long, stays correct even when the counter wraps around, after about 49 days of continuous operation.

with delay(500) stopped, blind obstacle appearsonly noticed herewith millis() IRdistance serial noticed immediately

How to read this map: Above, each red block is half a second during which the program is deaf and blind; the obstacle (amber line) is only noticed at the next reading. Below, three tasks run at their own rates: IR every 10 ms, distance every 60 ms, message every 250 ms. None waits for another, and the obstacle is noticed at the next distance reading.

Goal: combine an HC-SR04, a line-following IR sensor, and a warning LED in a single program without any delay in the loop. The GPIO 2 LED turns on when an obstacle is less than 20 cm away. This is the sensing skeleton for the rover in module 1.6.

sensores-sem-travar.ino
// Multiple sensors without blocking: millis() instead of delay (ESP32, 3.x)
const int PINO_TRIG = 5, PINO_ECHO = 18, PINO_LINHA = 35, PINO_LED = 2;
const float PERTO_CM = 20.0;

unsigned long tIR = 0, tDist = 0, tPrint = 0;
float distanciaCm = -1;
int linha = 0;

float medirCm() {
  digitalWrite(PINO_TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(PINO_TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(PINO_TRIG, LOW);
  unsigned long dur = pulseIn(PINO_ECHO, HIGH, 25000); // wait no more than 25 ms
  return (dur == 0) ? -1 : dur * 0.0343 / 2.0;
}

void setup() {
  Serial.begin(115200);
  pinMode(PINO_TRIG, OUTPUT); pinMode(PINO_ECHO, INPUT); pinMode(PINO_LED, OUTPUT);
  Serial.println("Sensors running without delay");
}

void loop() {
  unsigned long agora = millis();

  if (agora - tIR >= 10) {          // IR: 100 times per second
    tIR = agora;
    linha = analogRead(PINO_LINHA);
  }
  if (agora - tDist >= 60) {        // distance: ~16 times per second
    tDist = agora;
    float cm = medirCm();
    if (cm > 0) distanciaCm = cm;
    digitalWrite(PINO_LED, (distanciaCm > 0 && distanciaCm < PERTO_CM) ? HIGH : LOW);
  }
  if (agora - tPrint >= 250) {      // report: 4 times per second
    tPrint = agora;
    Serial.print("dist="); Serial.print(distanciaCm, 1);
    Serial.print(" cm | linha="); Serial.print(linha);
    Serial.println(distanciaCm > 0 && distanciaCm < PERTO_CM ? " | PERTO" : "");
  }
}

How to check

  • ✓The Monitor shows 4 lines per second with "dist=" and "linha="; bring your hand within 20 cm and "CLOSE" appears while the LED turns on immediately.
  • ✓Moving the line sensor over the black surface changes "linha=" immediately, even while distance is being measured.
  • ✗Everything is slow? Is there still a delay() in the loop or in a function it calls.

💡 pulseIn also waits

O pulseIn blocks until the echo returns, which is why the timeout is 25 ms: in the worst case, the loop loses 25 ms, never half a second. In Track 2, you’ll separate this completely with FreeRTOS tasks and interrupts.

Why learn this

A long delay leaves the robot blind, so it can crash before noticing an obstacle. With periodic tasks, the rover in module 1.6 measures, decides, and moves at the same time, and you can add sensors without rewriting the rest.

Key concepts

millis()
clock since startup
Periodic task
runs every X ms
Non-blocking
checks and continues
Timeout
maximum wait time

✅ Module summary

✓
Four languages: digital, analog, pulse width, and the I2C bus.
✓
HC-SR04: duration × 0.0343 ÷ 2; ECHO at 5 V through the 1 kΩ/2 kΩ divider.
✓
IR: light surfaces reflect, black surfaces absorb; calibrate on your floor and use the threshold between the readings.
✓
Filter: discard impossible readings, then use a short moving average.
✓
I2C: SDA 21, SCL 22, different addresses; the scanner checks the wiring.
✓
Non-blocking: millis() and periodic tasks instead of delay.

To go further

Next module

1.5 · Motors and power. DC, servo, or stepper motor; TB6612FNG or DRV8833 H-bridge; PWM speed control; servo with ESP32Servo; batteries and a common GND.