New here? This part adds an HC-SR04 ultrasonic sensor to the C101 robot from Part 13 — the car now stops, backs up, and steers around obstacles automatically. The wiring builds on the Raspberry Pi + L298N setup from Part 13. If the GPIO connections feel unfamiliar, a quick skim of Part 13 will help. Otherwise, let’s go.
The Loop Gets Its Second Step
In Part 13, the robot learned to move. That was Step 1 of the five-step autonomous loop:
Learn to Move → Perception → Localization → Planning → Control [repeat from Perception]
Today we add Step 2: Perception — the ability to sense the environment and react to it.
Until now, our C101 was essentially a very confident toddler: walking forward with complete disregard for whatever was in front of it. Wall? Doesn’t care. Chair leg? Doesn’t care. Expensive equipment on the floor? Definitely doesn’t care.
Time to give it some awareness.
We’ll keep everything from Part 13 exactly as it is — same car, same wiring, same Python base — and add one sensor: the HC-SR04. Same sensor we met in Part 11, but this time it’s mounted on a moving robot, and the Python brain actually responds to what it reports.
The goal: the car drives forward autonomously, detects any obstacle within 30cm, backs up, picks a random direction (left or right), steers clear, and keeps going. Repeat until you press Ctrl+C — or set a timer and let it run for exactly 20 seconds.
Wiring the HC-SR04 to the Raspberry Pi
Four connections, same as Part 11 — with one important difference: the ECHO pin needs a voltage divider to protect the Pi.
The HC-SR04 runs on 5V and its ECHO pin outputs 5V signals. The Raspberry Pi’s GPIO pins are only 3.3V tolerant. Feed 5V directly into a GPIO pin and you risk damaging the Pi permanently. The voltage divider steps it down to a safe ~3.3V.
The circuit (using 1kΩ and 2kΩ resistors):
ECHO (5V) ──── 1kΩ ──── GPIO 25 (Pi)
│
2kΩ
│
GND
Result: 5V × 2/(1+2) = 3.33V ✅ Safe for the Pi.
You can twist the wire ends around the resistor legs for a quick test — it works. Soldering makes it permanent and reliable.
Full wiring summary:
| HC-SR04 Pin | Connect To |
|---|---|
| VCC | Pi 5V |
| GND | Pi GND |
| TRIG | GPIO 17 (direct) |
| ECHO | GPIO 25 (via 1kΩ + 2kΩ voltage divider) |
The TRIG pin is fine connected directly — the Pi outputs 3.3V on GPIO pins, and that’s enough to trigger the HC-SR04.
Mounting the Sensor
Fitting the HC-SR04 onto the front of the C101 chassis is a small but satisfying puzzle. The cleanest solution: cut an ice cream stick to about 4 inches, slide it under the 7.4V battery pack (the battery’s weight holds it in place), mount the L298N on top, and attach the HC-SR04 to the front of the stick pointing forward.
The sensor needs a clear line of sight ahead — no wires hanging in front of it, no components blocking the beam. A few minutes of rearranging now saves a lot of confusing distance readings later.
The Python Code — Version 1: Runs Until You Stop It
This builds directly on the Part 13 code. New additions: TRIG and ECHO pin declarations, the get_distance() function, and the obstacle avoidance logic in the main loop.
import RPi.GPIO as GPIO
import time
import random
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
# L298N motor pins
ENA = 18; IN1 = 27; IN2 = 22
IN3 = 23; IN4 = 24; ENB = 13
# HC-SR04 pins
TRIG = 17
ECHO = 25
# Configure pins
for pin in [ENA, ENB, IN1, IN2, IN3, IN4, TRIG]:
GPIO.setup(pin, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
# PWM setup
pwm_A = GPIO.PWM(ENA, 50)
pwm_B = GPIO.PWM(ENB, 50)
pwm_A.start(0)
pwm_B.start(0)
def get_distance():
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
start_time = time.time()
stop_time = time.time()
while GPIO.input(ECHO) == 0:
start_time = time.time()
while GPIO.input(ECHO) == 1:
stop_time = time.time()
# Speed of sound ~34300 cm/s, divide by 2 for one-way distance
distance = (stop_time - start_time) * 34300 / 2
return distance
def set_motor(in1, in2, pwm, speed):
GPIO.output(in1, GPIO.HIGH if speed >= 0 else GPIO.LOW)
GPIO.output(in2, GPIO.LOW if speed >= 0 else GPIO.HIGH)
pwm.ChangeDutyCycle(abs(speed))
def forward(speed):
set_motor(IN1, IN2, pwm_A, speed)
set_motor(IN3, IN4, pwm_B, speed)
def backward(speed):
set_motor(IN1, IN2, pwm_A, -speed)
set_motor(IN3, IN4, pwm_B, -speed)
def turn_left(speed):
set_motor(IN1, IN2, pwm_A, -speed)
set_motor(IN3, IN4, pwm_B, speed)
def turn_right(speed):
set_motor(IN1, IN2, pwm_A, speed)
set_motor(IN3, IN4, pwm_B, -speed)
def stop_car():
set_motor(IN1, IN2, pwm_A, 0)
set_motor(IN3, IN4, pwm_B, 0)
try:
print("Moving. Press Ctrl+C to stop.")
speed = 70
while True:
dist = get_distance()
print(f"Distance: {dist:.2f} cm")
if dist < 30:
print("Obstacle detected! Stopping and reversing...")
stop_car()
backward(50)
time.sleep(1)
stop_car()
direction = random.choice(['left', 'right'])
if direction == 'left':
print("Steering left...")
turn_left(75)
else:
print("Steering right...")
turn_right(75)
time.sleep(0.8)
stop_car()
else:
forward(speed)
time.sleep(0.1)
except KeyboardInterrupt:
print("\nStopped by user.")
finally:
stop_car()
pwm_A.stop()
pwm_B.stop()
GPIO.cleanup()
print("GPIO cleaned up.")
How the distance formula works:
distance = (time_elapsed × 34300) / 2
The HC-SR04 fires a sound pulse and measures how long it takes to bounce back. Sound travels at ~34,300 cm/s. We divide by 2 because the sound makes a round trip — out to the obstacle and back. If the echo takes 0.002 seconds to return, the obstacle is (0.002 × 34300) / 2 = 34.3 cm away.
Version 2: Runs for Exactly 20 Seconds
Sometimes you want the robot to run a timed test rather than loop forever. Add a start_run_time check at the top of the loop:
import RPi.GPIO as GPIO
import time
import random
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
ENA = 18; IN1 = 27; IN2 = 22
IN3 = 23; IN4 = 24; ENB = 13
TRIG = 17; ECHO = 25
for pin in [ENA, ENB, IN1, IN2, IN3, IN4, TRIG]:
GPIO.setup(pin, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
pwm_A = GPIO.PWM(ENA, 50)
pwm_B = GPIO.PWM(ENB, 50)
pwm_A.start(0)
pwm_B.start(0)
def get_distance():
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
start_time = stop_time = time.time()
while GPIO.input(ECHO) == 0:
start_time = time.time()
while GPIO.input(ECHO) == 1:
stop_time = time.time()
return (stop_time - start_time) * 34300 / 2
def set_motor(in1, in2, pwm, speed):
GPIO.output(in1, GPIO.HIGH if speed >= 0 else GPIO.LOW)
GPIO.output(in2, GPIO.LOW if speed >= 0 else GPIO.HIGH)
pwm.ChangeDutyCycle(abs(speed))
def forward(s): set_motor(IN1, IN2, pwm_A, s); set_motor(IN3, IN4, pwm_B, s)
def backward(s): set_motor(IN1, IN2, pwm_A, -s); set_motor(IN3, IN4, pwm_B, -s)
def turn_left(s): set_motor(IN1, IN2, pwm_A, -s); set_motor(IN3, IN4, pwm_B, s)
def turn_right(s): set_motor(IN1, IN2, pwm_A, s); set_motor(IN3, IN4, pwm_B, -s)
def stop_car(): set_motor(IN1, IN2, pwm_A, 0); set_motor(IN3, IN4, pwm_B, 0)
try:
print("Running for 20 seconds. Ctrl+C to stop early.")
speed = 70
start_run_time = time.time()
run_duration = 20 # Change this to 30, 60, whatever you like
while True:
if time.time() - start_run_time >= run_duration:
print("Time's up. Stopping.")
break
dist = get_distance()
print(f"Distance: {dist:.2f} cm")
if dist < 30:
print("Obstacle! Reversing...")
stop_car()
backward(50)
time.sleep(1)
stop_car()
direction = random.choice(['left', 'right'])
turn_left(75) if direction == 'left' else turn_right(75)
print(f"Steering {direction}...")
time.sleep(0.8)
stop_car()
else:
forward(speed)
time.sleep(0.1)
except KeyboardInterrupt:
print("\nInterrupted.")
finally:
stop_car()
pwm_A.stop()
pwm_B.stop()
GPIO.cleanup()
print("Done.")
Change run_duration = 20 to any number of seconds you like.
What We Just Built
The robot now completes two steps of the autonomous loop:
✅ Learn to Move — moves on command (Part 13)
✅ Perception — senses obstacles and reacts (Part 14)
One $4 sensor. About 20 extra lines of Python. And the car goes from "drives until it hits something" to "drives, detects, avoids, continues." That’s a meaningful jump.
The avoidance logic is simple — random left or right, no analysis of which direction is actually clearer. That’s intentional. Keeping it simple here makes the next upgrade more satisfying: in Part 15, the camera will look left and right and choose the better direction rather than guessing.