ping-rs: Servo-Swept Ultrasonic Radar

ping-rs is a small robotics sensing stack on STM32F411RE in async Rust. An HC-SR04 ultrasonic rangefinder rides on an SG90 servo, sweeps the forward field, renders a polar view on an SSD1306 OLED, and streams timestamped samples over USB CDC.

The useful part is not the sensor. It is the whole embedded loop: deterministic pulse timing, servo position tracking, bounded channels, display refresh, host telemetry, and explicit handling for stale samples.

Goal

Build a readable no-std example of a scanning range sensor that can feed later robotics projects:

  • generate servo PWM without blocking range capture,
  • measure ultrasonic echo pulse width with microsecond timing,
  • associate each range sample with the best-known servo angle,
  • render a live polar plot on-device,
  • export JSON lines to a host for plotting and log review,
  • keep scanning even if USB or display output stalls.

Hardware

Part Purpose
STM32 Nucleo-F411RE Cortex-M4F target and onboard ST-Link
HC-SR04 Ultrasonic time-of-flight rangefinder
SG90 180 degree micro servo
SSD1306 0.96 inch OLED 128 by 64 polar display over I2C
USB CDC ACM Host telemetry stream
Logic analyzer Pulse, PWM, and I2C timing capture

Wiring

Signal Nucleo pin MCU pin
Servo PWM CN5 D1 PA0, TIM2_CH1
HC-SR04 trigger CN8 A1 PA1
HC-SR04 echo CN9 A3 PB0
OLED SCL CN10 D15 PB8, I2C1_SCL
OLED SDA CN10 D14 PB9, I2C1_SDA
USB D+/D- USB PA11 / PA12
Status LED onboard PA5, LD2

The HC-SR04 is powered from 5 V. Its echo output is divided to 3.3 V with a 1 kΩ / 2 kΩ divider before PB0.

Scan loop

The servo sweeps 0 to 180 to 0 degrees over roughly six seconds per full scan. The range task fires one ultrasonic ping every 50 ms. Each sample carries:

{"t_us":1234567,"angle_deg":45,"dist_mm":823}

Angle is sampled from an atomic value maintained by the servo task. That is accurate enough for this sensor because the sonar beam width and servo backlash dominate sub-degree timing error.

Software architecture

servo::sweep_task
    |
    v
CURRENT_ANGLE, AtomicU8
    |
    v
ultrasonic::ping_task
    |
    v
Sample channel, depth 64
    |                         |
    v                         v
display::render_task          telemetry::usb_task
SSD1306 polar plot            JSON over USB CDC

The sample channel uses nonblocking send. If display or USB stalls, new samples may be dropped rather than backing up the scan loop. For a live radar view, freshness is more valuable than replaying stale points.

Timing constraints

Path Constraint
Servo PWM stable 50 Hz frame, pulse width mapped to angle
Trigger pulse at least 10 µs high
Echo capture timeout on no return, no unbounded wait
Ping period at least 50 ms to avoid acoustic overlap
Display bounded redraw, no blocking in range task
Telemetry best-effort stream, no scan backpressure

The logic-analyzer proof point is a capture with trigger pulse, echo pulse, servo PWM, and one debug GPIO marker around the range measurement. That separates sensor timing bugs from rendering or USB bugs.

Error sources

  • HC-SR04 beam width creates angular smear.
  • Soft targets and angled surfaces return weak or late echoes.
  • Servo backlash means commanded angle is not exact shaft angle.
  • Temperature changes sound speed.
  • USB logging can perturb timing if it is allowed to block the measurement path.

The code treats the output as a proximity map, not a metrology instrument.

Build and flash

rustup target add thumbv7em-none-eabihf
cargo install probe-rs-tools
cargo run --release

RTT logs use the ST-Link probe. USB CDC is reserved for sample telemetry.

STM32 Bare-Metal Bring-Up, Interrupts, DMA, and Ring Buffers, Measurement and Instrumentation, and PID Tuning and Step Response.

view on github