Detailed content
🗣️ 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.
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 kit | Language | Power | Caution |
|---|---|---|---|
| HC-SR04 | pulse width | 5 V | ECHO returns 5 V: use a divider |
| Obstacle IR | digital | 3,3 V | adjust the trimpot |
| TCRT5000 (line) | analog (AO) or digital (DO) | 3,3 V | use an ADC1 pin |
| VL53L0X, MPU-6050 | I2C | 3.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
🦇 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.
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
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.
TRIG direct
TRIG to GPIO 5 with a simple jumper wire.
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: 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
🔦 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.
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.
// 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
📉 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.
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.
// 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
🔗 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.
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
Connect four wires
VCC → 3.3 V, GND → GND, SDA → GPIO 21, SCL → GPIO 22.
Run the scanner
It checks all 127 addresses to see which ones respond. If your sensor appears, the wiring is correct.
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: 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 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
⏱️ 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.
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.
// 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
✅ Module summary
To go further
- I2C on Arduino-ESP32 (Espressif) — Wire, pins, and speed
- Pololu VL53L0X Library — examples and measurement modes
- HC-SR04 on Wokwi — simulate the distance sensor
- Blink Without Delay (Arduino) — the millis() pattern explained
- ADC on Arduino-ESP32 — analog readings from IR sensors
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.