Ultrasonic Sonar Scanner with Raspberry Pi Pico Clone

Description

On one sleeeless night, I was browsing deals on Aliexpress, like one usually does. A bundle deal offered me an Ultrasonic Module HC-SR04, which was a very interesting deal. But I had no idea what I wanted to build with it. A few clicks later, after I saw a crazy $3 offer for a Raspberry Pi Pico W clone, I knew exactly what I wanted to build! What about a rotating sonar scanner? Sounds interesting, right? Lastly, I snapped up a small servo and a motor driver, just in case I wanted to try some leftover DC motors I have.

Project roadmap:

  • On the Pico, separate the web server and servo control / sensor reading into different cores.
  • Build a frontend. We want a nice UI that will periodically fetch data from our Pico and display the current reading and servo rotation.

Raspberry Pi Pico

A very interesting microcontroller… you can code it in MicroPython to make things easy, and you even have 2 cores on it. But I don’t really understand the decision to operate it on 3.3V. Maybe the goal is a minimal setup powered by one small 3.7V battery? My point is, most things I have operate on 5V minimum, so I need to be careful. Later, after discovering all the secrets of the Pico, I understood why this clone is so much cheaper than the original… but more about that later.

The servo made this project much easier. After reading the documentation on how to use it, all I needed to do was write a function that rotates the servo to the angle I need. The code for the ultrasonic sensor is copy-paste from the documentation too: set high/low on the trigger pin and then catch the echo. The last thing I needed was to use the built-in Pico library for WiFi, and I would be done…

And this is the point where I discovered why this clone is so cheap. It’s not a Pico W… it’s a Pico with a soldered ESP WiFi chip on the board. Oh boy…

So my WiFi functionality is limited, but for this project it’s sufficient. To communicate with the WiFi chip on clone boards, you need to start a UART connection. Check your board, on mine, I had markings for ESP TX and ESP RX next to pins 0 and 1.

Frontend

Don’t overcomplicate things. It’s simple HTML/JS code, and I fetch data from the Pico every 150–300 ms.

Code

Frontend.zip

4.4 Kb

MicroPython
import machine
import time
import _thread
import math
from machine import UART, Pin, PWM, time_pulse_us

# ==========================================
# 1. CONFIGURATION
# ==========================================

# --- Wi-Fi Settings ---
SSID = "PiedPiper"
PASSWORD = "ErlichBachmanIsAFat"

# --- Servo Calibration ---
# Freq: 50Hz. Standard duty is 1ms to 2ms (approx 1638 to 8192 in u16).
# Depending on your specific servo, you might need to widen these ranges.
SERVO_PIN = 15
SERVO_MIN_US = 1638  # Value for 0 degrees
SERVO_MAX_US = 8192  # Value for 180 degrees
SCAN_DELAY = 0.1    # Time to wait for sensor between steps

# --- Ultrasonic Pins ---
TRIG_PIN = 16
ECHO_PIN = 17

# ==========================================
# 2. SHARED GLOBAL VARIABLES
# ==========================================

servo = PWM(Pin(SERVO_PIN))
servo.freq(50)
trig = Pin(TRIG_PIN, Pin.OUT)
echo = Pin(ECHO_PIN, Pin.IN)

# Thread Lock to prevent reading/writing at same time
data_lock = _thread.allocate_lock()

# The data to be served via JSON
shared_data = {
    "dis": 0,    # Distance in cm
    "rot": 0     # Current Angle (0-180)
}

# ==========================================
# 3. CORE 1: MOTOR & SENSOR CONTROL
# ==========================================

def set_angle(angle):
    duty = SERVO_MIN_US + (SERVO_MAX_US - SERVO_MIN_US) * (angle / 180)
    servo.duty_u16(int(duty))

def get_distance():
    dist_cm = 0
    try:
        trig.low()
        time.sleep_us(2)
        trig.high()
        time.sleep_us(10)
        trig.low()

        # Timeout 30ms (approx 5 meters max) to prevent blocking
        duration = time_pulse_us(echo, 1, 30000) 

        if duration > 0:
            dist_cm = int((duration / 2) / 29.1)
            if dist_cm > 200: dist_cm = 200 # Cap max distance
        else:
            dist_cm = -1 # Out of range
    except OSError:
        dist_cm = -1
    return dist_cm


def core1_task():
    trig.low()
    time.sleep(0.1)

    print("[Core 1] Radar Scan Started")

    current_angle = 0
    accm = 1

    while True:
        set_angle(current_angle)
        time.sleep(SCAN_DELAY)

        dist_cm = get_distance()
        if current_angle >= 180: accm = -1
        if current_angle <= 0: accm = 1

        current_angle += accm

        with data_lock:
            shared_data["dis"] = dist_cm
            shared_data["rot"] = current_angle
            
        print(shared_data)

# ==========================================
# 4. CORE 0: WEB SERVER
# ==========================================

uart = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))

def send_at(cmd, wait=1):
    uart.write(cmd + '\r\n')
    time.sleep(wait)
    if uart.any(): return uart.read()
    return b""

def init_wifi():
    print("[Core 0] Init Wi-Fi...")
    send_at("AT+RST", 2)       # Reset module
    send_at("AT+CWMODE=1")
    send_at(f'AT+CWJAP="{SSID}","{PASSWORD}"', 8)
    ip_info = send_at("AT+CIFSR", 2)
    print("[Core 0] IP info:", ip_info)
    send_at("AT+CIPMUX=1")
    send_at("AT+CIPSERVER=1,80")
    print("[Core 0] Server Ready.")

def server_loop():
    buffer = ""

    while True:
        if uart.any():
            try:
                chunk = uart.read().decode('utf-8', 'ignore')
                buffer += chunk

                # Check for Request
                if "+IPD," in buffer:
                    # Parse Connection ID (usually the char after +IPD,)
                    idx = buffer.find("+IPD,")
                    conn_id = buffer[idx + 5]

                    # --- READ SHARED DATA SAFELY ---
                    local_dis = 0
                    local_rot = 0

                    with data_lock:
                        local_dis = shared_data["dis"]
                        local_rot = shared_data["rot"]

                    # --- BUILD RESPONSE ---
                    # We are sending the simplest Valid JSON
                    json_body = '{"dis":' + str(local_dis) + ',"rot":' + str(local_rot) + '}'

                    header = "HTTP/1.1 200 OK\r\n"
                    header += "Content-Type: application/json\r\n"
                    header += "Access-Control-Allow-Origin: *\r\n" # CORS
                    header += "Connection: close\r\n"
                    header += "Content-Length: " + str(len(json_body)) + "\r\n\r\n"

                    full_resp = header + json_body

                    # Send Command
                    uart.write(f"AT+CIPSEND={conn_id},{len(full_resp)}\r\n")
                    time.sleep(0.02) # Short wait for prompt >
                    uart.write(full_resp)

                    # Close Command
                    time.sleep(0.02)
                    uart.write(f"AT+CIPCLOSE={conn_id}\r\n")

                    buffer = "" # Clear buffer for next request

            except Exception as e:
                # If UART garbage causes error, reset buffer
                print("Error:", e)
                buffer = ""

# ==========================================
# 5. EXECUTION START
# ==========================================

print("Reset radar position")
set_angle(0)
time.sleep(0.5)

# Run Core 0 (Wi-Fi) in main thread
init_wifi()
time.sleep(10)
# Start Core 1 (Servo/Sensor) in background
_thread.start_new_thread(core1_task, ())
server_loop()

Sources

JiriBilek

WiFi library for RP2040 Pico-W with the ESP8285 chip

https://github.com/JiriBilek/RP2040_PicoW_ESP8285_Library

The Engineering Mindset

Servo Motors, how do they work?

https://youtu.be/1WnGv-DPexc

Aliexpress