A Raspberry Pi radar project maps physical space by sweeping an ultrasonic sensor across a 180-degree arc and plotting the distance returns on a polar coordinate graph. This guide targets the Raspberry Pi 4 Model B (4GB) and the Raspberry Pi 5, using the modern gpiozero library and matplotlib for the GUI. We will cover the mandatory 5V-to-3.3V logic step-down, exact pin mappings, and a fully compilable Python script with error handling.

Hardware Specifications & Parts List

Before wiring, you must understand the logic-level mismatch between the Pi and the sensor. The Raspberry Pi GPIO pins operate at 3.3V. The HC-SR04 Echo pin outputs 5V. Feeding 5V directly into a Pi GPIO will eventually degrade or destroy the SoC pin. A voltage divider is non-negotiable.

Table 1: Component Specifications & Electrical Limits
Component Variant / Model Operating Voltage Logic / Signal Level Key Limitation
Microcontroller Raspberry Pi 4B / 5 5V (via USB-C) 3.3V CMOS Max 50mA per GPIO pin
Ultrasonic Sensor HC-SR04 5V DC 5V TTL (Echo) Requires voltage divider on Echo
Servo Motor SG90 Micro Servo 4.8V - 6.0V DC 3.3V PWM (50Hz) Stalls at >1.8kg-cm torque
Resistors 1kΩ and 2kΩ (1/4W) N/A N/A Tolerance ±5% is acceptable
Bench Tip: If you don't have a 2kΩ resistor, use two 1kΩ resistors in series. The ratio (R2 / (R1 + R2)) must be approximately 0.66 to drop 5V down to a safe 3.3V.

Pin Mapping & Physical Assembly

Wire the components on a half-size breadboard. Mount the SG90 servo to a fixed base and attach the HC-SR04 to the servo horn using hot glue or a 3D-printed bracket.

Table 2: Raspberry Pi GPIO Pin Mapping (BCM Numbering)
Sensor / Servo Pin Pi Physical Pin Pi BCM GPIO Wiring Notes
HC-SR04 VCC Pin 2 5V Power Direct to 5V rail
HC-SR04 GND Pin 6 Ground Common ground with Pi and Servo
HC-SR04 Trig Pin 16 GPIO 23 Direct connection (3.3V is enough to trigger 5V sensor)
HC-SR04 Echo Pin 18 GPIO 24 Must pass through 1kΩ/2kΩ voltage divider
SG90 PWM (Orange) Pin 11 GPIO 17 Direct PWM signal
SG90 VCC (Red) Pin 4 5V Power Use separate 5V supply if Pi brownouts occur
SG90 GND (Brown) Pin 9 Ground Common ground

Python Polar Plot Radar Code

This script uses gpiozero for hardware abstraction and matplotlib to render an animated polar plot. It includes try/except blocks to handle sensor timeouts and safe GPIO cleanup.


import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from gpiozero import DistanceSensor, AngularServo
from time import sleep
import sys

# --- Pin Definitions (BCM) ---
TRIG_PIN = 23
ECHO_PIN = 24
SERVO_PIN = 17

# --- Hardware Initialization ---
# max_distance=4.0 sets the timeout threshold to prevent infinite hangs
try:
    sensor = DistanceSensor(echo=ECHO_PIN, trigger=TRIG_PIN, max_distance=4.0)
    # SG90 pulse widths: min=1ms (0 deg), max=2ms (180 deg)
    servo = AngularServo(SERVO_PIN, min_angle=0, max_angle=180, 
                         min_pulse_width=0.001, max_pulse_width=0.002)
except Exception as e:
    print(f'Hardware Init Error: {e}')
    sys.exit(1)

# --- Matplotlib Polar Setup ---
fig = plt.figure(figsize=(7, 7), facecolor='#111111')
ax = fig.add_subplot(111, polar=True, facecolor='#000000')
ax.set_ylim(0, 400)  # Max range 400cm
ax.set_yticks([50, 100, 200, 300])
ax.set_yticklabels(['50cm', '100cm', '200cm', '300cm'], color='white')
ax.set_xticklabels(['90°', '60°', '30°', '0°', '330°', '300°', '270°'], color='white')
ax.grid(color='green', linestyle=':', linewidth=0.5)

line, = ax.plot([], [], 'o-', color='#00ff00', linewidth=2, markersize=4)

# Data storage for the sweep
angles = []
distances = []

def update_radar(frame):
    global angles, distances
    
    # Sweep logic: 0 to 180 degrees
    current_angle = frame % 181
    servo.angle = current_angle
    sleep(0.03)  # Allow servo to settle and sensor to ping
    
    try:
        # gpiozero returns distance in meters; convert to cm
        dist_cm = sensor.distance * 100
        if dist_cm > 400 or dist_cm < 2:
            dist_cm = 400  # Treat out-of-bounds as max range
    except Exception:
        dist_cm = 400  # Fallback on timeout/error
        
    angles.append(np.radians(current_angle))
    distances.append(dist_cm)
    
    # Keep only the last 181 points (one full sweep)
    if len(angles) > 181:
        angles.pop(0)
        distances.pop(0)
        
    line.set_data(angles, distances)
    return line,

# Run animation
ani = animation.FuncAnimation(fig, update_radar, frames=range(360), 
                              interval=50, blit=False, repeat=True)

try:
    plt.title('Raspberry Pi Ultrasonic Radar', color='white', pad=20)
    plt.show()
except KeyboardInterrupt:
    print('Radar stopped by user.')
finally:
    servo.detach()  # Stop PWM signal to prevent servo jitter
    print('GPIO cleaned up.')

Debugging Common Radar Failures

When your radar fails to initialize or the GUI freezes, check these exact error strings and their root causes.

1. The "No access to /dev/mem" Error

Exact Error String: RuntimeError: No access to /dev/mem. Try running as root!

Ranked Causes:

  1. Legacy Library on Bookworm OS: You are using the deprecated RPi.GPIO library on Raspberry Pi OS Bookworm, which dropped support for it. Fix: Switch to gpiozero as shown in the code above.
  2. Missing Permissions: Your user isn't in the gpio group. Fix: Run sudo usermod -aG gpio $USER and reboot.

2. The Pi 5 Pin Factory Error

Exact Error String: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!

Ranked Causes:

  1. Missing lgpio Backend: The Raspberry Pi 5 requires the lgpio backend for gpiozero to function. Fix: Run sudo apt install python3-rpi-lgpio.
  2. Virtual Environment Isolation: You are running inside a Python venv that lacks system site-packages. Fix: Recreate the venv with python3 -m venv --system-site-packages env.

3. The First Three Things to Check When Hardware Fails

If the code runs but the radar plot is empty or the Pi reboots randomly:

  1. Multimeter the Voltage Divider: Disconnect the Echo pin from the Pi. Trigger the sensor and measure the voltage at the junction of the 1kΩ and 2kΩ resistors. It must read between 3.2V and 3.3V. If it reads 5V, your resistor wiring is wrong and you are risking the Pi.
  2. Check for 5V Rail Brownout: The SG90 servo draws up to 700mA under stall. If powered directly from the Pi's 5V pin, it will pull the voltage down, causing the Pi's low-voltage warning (lightning bolt icon) and random reboots. Fix: Power the servo from a dedicated 5V 2A buck converter or USB power bank, tying only the GND to the Pi.
  3. Verify Sensor Timeout Threshold: If the sensor points at a soft surface (like a curtain) that absorbs sound waves, it will timeout. Ensure max_distance=4.0 is set in gpiozero so the library handles the timeout gracefully instead of hanging the thread.

Extending or Simplifying the Build

Depending on your application, you can scale this raspberry pi radar project up or down.

How to Simplify (Headless Mode):
If you don't need the GUI and just want to log proximity data for a security alarm, strip out matplotlib entirely. Replace the animation loop with a simple while True: loop that writes sensor.distance and servo.angle to a CSV file or publishes it via MQTT to Home Assistant. This reduces CPU load to near zero and allows the script to run as a background systemd service.

How to Extend (mmWave Upgrade):
The HC-SR04 struggles with soft materials and wide beam angles (15°). For a 2026-spec upgrade, replace the ultrasonic sensor with an HLK-LD2410 mmWave Radar Sensor. The LD2410 communicates via UART (TX/RX pins) and provides gating data for both moving and stationary targets up to 6 meters. You will need to swap the DistanceSensor class for a custom pyserial parser to read the hex frames, but the physical servo sweep and polar plotting logic remain identical. For more on serial communication protocols, refer to the gpiozero documentation and matplotlib subplot references.