# Giving My Robot Its First Sense: HC-SR04 Ultrasonic on Arduino UNO Q

In the [last post](https://prahari.net/arduino-uno-q-is-not-a-regular-arduino-what-i-learned-the-hard-way), I got the Arduino UNO Q working from the command line — blink LED, SSH, deploy apps.

Now it's time to give the board its first real input: distance sensing with an HC-SR04 ultrasonic sensor. This is the sensor that will eventually let my eldercare robot detect walls, furniture, and doorways as it patrols a home.

But first — can `pulseIn()` even work on Zephyr RTOS?

## Why This Matters

HomeGuard Parivaar needs to navigate rooms autonomously. That means obstacle detection. The HC-SR04 is the cheapest, most reliable way to measure distance — if it works on the UNO Q's STM32 MCU running Zephyr.

The interesting part isn't the sensor itself (it's well-documented everywhere). It's the **pattern**:

*   The MCU reads the sensor in its own tight loop
    
*   The Python side on the MPU polls for data via Bridge whenever it needs it
    
*   Neither side blocks the other
    

This decoupled architecture is how all of the robot's sensors will work.

## What We're Building

By the end of this post, you'll have:

*   An HC-SR04 wired to the UNO Q's MCU pins
    
*   A sketch that reads distance continuously and exposes it via Bridge
    
*   A Python script that polls and prints distance readings
    
*   Confidence that timing-sensitive Arduino functions work under Zephyr
    

## Hardware You'll Need

| Component | Notes |
| --- | --- |
| Arduino UNO Q | CLI setup complete ([see Part 1](https://prahari.net/arduino-uno-q-is-not-a-regular-arduino-what-i-learned-the-hard-way)) |
| HC-SR04 ultrasonic sensor | ~$2, widely available |
| 4x jumper wires (F-M) | VCC, GND, TRIG, ECHO |

![](https://hashnode-media.s3.amazonaws.com/homeguard/blog/media/002/lab-1.3-hcsr04-sensor.jpg align="center")

I used a breadboard to keep the wiring tidy.

## Wiring

| HC-SR04 Pin | UNO Q Pin |
| --- | --- |
| VCC | 5V |
| GND | GND |
| TRIG | D2 |
| ECHO | D3 |

**No voltage divider needed.** The STM32U585's digital pins are 5V tolerant (except A0/A1 — a detail from Lab 1.1 that saved me a resistor here).

![](https://hashnode-media.s3.amazonaws.com/homeguard/blog/media/002/lab-1.3-hcsr04-wiring.jpg align="center")

## The App Structure

Same pattern as the blink app from Part 1 — an `app.yaml`, a sketch folder, and a Python folder:

```plaintext
q-sonar/
├── app.yaml
├── sketch/
│   ├── sketch.ino
│   └── sketch.yaml
└── python/
    ├── main.py
    └── requirements.txt
```

## The MCU Sketch

The MCU does two things:

*   Reads the sensor in `loop()` every 100ms
    
*   Exposes the last reading via a Bridge function
    

```cpp
#include "Arduino_RouterBridge.h"

const int TRIG_PIN = 2;
const int ECHO_PIN = 3;

float last_distance_cm = -1.0;

void setup() {
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    Bridge.begin();
    Bridge.provide("get_distance", get_distance);
}

void loop() {
    last_distance_cm = read_distance();
    delay(100);
}

float read_distance() {
    digitalWrite(TRIG_PIN, LOW);
    delayMicroseconds(2);
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG_PIN, LOW);

    long duration = pulseIn(ECHO_PIN, HIGH, 30000);  // 30ms timeout
    if (duration == 0) {
        return -1.0;  // No echo — out of range
    }
    return duration * 0.0343 / 2.0;  // cm
}

float get_distance() {
    return last_distance_cm;
}
```

A few things to note:

*   `pulseIn()` **with a 30ms timeout** — this caps the max range at ~5 meters (plenty for indoor rooms) and prevents the sketch from hanging if nothing echoes back.
    
*   `0.0343 / 2.0` — speed of sound in cm/us, divided by 2 because the pulse travels to the object and back.
    
*   **The** `loop()` **is NOT empty this time.** Unlike the blink app where the MCU just waited for Bridge calls, here the MCU actively samples the sensor. The Bridge function `get_distance()` just returns the latest cached reading.
    

## The Python Side

```python
from arduino.app_utils import *
import time

def loop():
    distance = Bridge.call("get_distance")
    if distance < 0:
        print("No echo — out of range")
    else:
        print(f"Distance: {distance:.1f} cm")
    time.sleep(1)

App.run(user_loop=loop)
```

The Python side polls once per second. The MCU samples 10x per second.

This means the Python side always gets a fresh reading without needing to worry about sensor timing.

## Deploy and Run

```bash
# Copy app to the board
ssh arduino-2gb 'mkdir -p ~/ArduinoApps/q_sonar'
scp -r q-sonar/* arduino-2gb:~/ArduinoApps/q_sonar/

# Start it
ssh arduino-2gb 'arduino-app-cli app start ~/ArduinoApps/q_sonar'
```

![App compile, flash, and start](https://hashnode-media.s3.amazonaws.com/homeguard/blog/media/002/lab-1.3-app-start.png align="center")

Check the logs:

```bash
ssh arduino-2gb 'arduino-app-cli app logs ~/ArduinoApps/q_sonar'
```

```plaintext
Distance: 46.3 cm
Distance: 137.2 cm
Distance: 44.6 cm
Distance: 48.0 cm
Distance: 17.1 cm
Distance: 138.9 cm
```

![App logs showing distance readings](https://hashnode-media.s3.amazonaws.com/homeguard/blog/media/002/lab-1.3-app-logs.png align="center")

Move your hand in front of the sensor — near readings (~17cm) and far readings (~137cm to the wall) both respond correctly.

## What Surprised Me

**1.** `pulseIn()` **works perfectly on Zephyr.** This was my main concern going in. Timing-sensitive functions can behave unpredictably under an RTOS, but the Arduino Zephyr core handles it cleanly. No jitter, no missed pulses.

**2\. I wired it wrong first.** TRIG and ECHO were swapped. The sketch deployed fine, but every reading came back as "No echo — out of range."

I spent a few minutes reading the code looking for a Zephyr compatibility issue... when it was just two wires in the wrong pins. Check your wiring before debugging your code.

**3\. No external libraries needed.** HC-SR04 only uses `digitalRead`, `digitalWrite`, and `pulseIn` — all built into the Arduino core for Zephyr. The only dependency is `Arduino_RouterBridge` for the Bridge pattern.

**4\. The MCU loop + Bridge pattern is the right architecture.** The MCU samples at its own pace. The Python side reads when it needs to. Neither blocks the other.

This is exactly how the robot will work — the MCU manages real-time sensor reads, the MPU handles decision logic. One pattern, many sensors.

## What's Next

The sensor works. The Bridge pattern works.

Next up: connecting a USB webcam to the MPU's Linux side — giving the robot eyes to go with its sonar. After that, bridging sensor data and camera feeds together for the robot's perception layer.

Follow along as I build an eldercare robot, one sensor at a time.

* * *

*This is part of my journey building* *HomeGuard Parivaar* *— an autonomous eldercare robot for Indian families, built with* [*Arduino UNO Q.*](https://store.arduino.cc/products/uno-q)

*This is a hobby project and I'm learning by building. If you have suggestions, corrections, or criticism — I'd genuinely love to hear it.*

*Co-authored with* [*Claude Code*](https://claude.com/product/claude-code) *(Anthropic) — my AI pair-programming partner for this build. Cover image generated with* [*Gemini*](https://gemini.google.com) *(Google).*
