The Physics of Acoustic Raster Scanning

Arduino ultrasonic imaging relies on the same fundamental physics as medical sonar or submarine acoustics: emitting a 40kHz piezoelectric pulse, timing the echo return, and calculating distance based on the speed of sound in air (approximately 343 m/s at 20°C). However, unlike a simple obstacle-avoidance robot, an imaging rig requires spatial awareness. By mounting a single transducer on a 2-axis pan-tilt gimbal, we can perform a raster scan—sweeping horizontally and vertically to build a 2D depth map (a matrix of distance values) that represents the physical topology of the space in front of the sensor.

The primary challenge in hobbyist acoustic imaging is beam divergence. A standard 40kHz transducer does not emit a laser-thin beam; it projects a conical lobe (typically 15° to 30° wide). When this cone hits an angled surface, the sound scatters, and side-lobes can reflect off adjacent objects, creating 'ghost' pixels in your final image. To mitigate this, we must select the right sensor, isolate our servo power to prevent logic brownouts, and use a microcontroller with enough SRAM to buffer the image matrix.

Sensor Selection: Why Beam Angle Dictates Resolution

Before wiring anything, you must choose a transducer that matches your imaging distance. The cheap HC-SR04 is nearly useless for imaging due to its wide 30° beam angle and poor acoustic coupling. Below is a data-dense comparison of the three most common 40kHz sensors used in DIY acoustic cameras.

Sensor Model Beam Angle (-6dB) Blind Zone Resolution Max Range Approx. Cost (2026)
HC-SR04 (Standard) ~30° 2 cm 3 mm 400 cm $1.50
JSN-SR04T v2.0 (Waterproof) ~15° 20 cm 1 mm 600 cm $8.00
MaxBotix MB7389 (HRXL) ~10° 0 cm (No blind zone) 1 mm 500 cm $110.00
RCWL-1601 (Ultrasonic) ~20° 2 cm 3 mm 300 cm $3.50

The Verdict: For this build, we are using the JSN-SR04T v2.0. Its 15° beam angle provides vastly superior lateral resolution for imaging compared to the HC-SR04. However, note the 20cm blind zone caused by the acoustic ringing of the waterproof membrane. If your target application requires imaging objects closer than 20cm, you must absorb the $110 cost of the MaxBotix MB7389.

Hardware BOM and Pin Mapping

We are targeting the Arduino Mega 2560 Rev3. Why not the Uno? A 40x40 pixel image buffer requires storing 1,600 distance readings. Using 16-bit integers (uint16_t), that is 3,200 bytes of SRAM. The Uno only has 2,048 bytes total; the Mega has 8,192 bytes, leaving plenty of headroom for the stack and serial buffers.

Parts List

  • MCU: Arduino Mega 2560 Rev3 (ATmega2560)
  • Sensor: JSN-SR04T v2.0 Ultrasonic Module
  • Gimbal: Standard Pan-Tilt Servo Bracket Kit
  • Servos: 2x MG996R High-Torque Metal Gear Servos (5V-7.4V)
  • Power: 5V 3A Buck Converter (BEC) or dedicated 5V 3A wall supply
  • Wiring: 22 AWG silicone wire, M-F Dupont connectors

Pin Mapping Table

Component Pin / Wire Arduino Mega 2560 Pin Notes
Pan Servo (X-axis) Signal (Orange) D2 (PWM) Must use PWM-capable pin
Tilt Servo (Y-axis) Signal (Orange) D3 (PWM) Must use PWM-capable pin
JSN-SR04T Trig D4 Digital Output
JSN-SR04T Echo D5 Digital Input (5V tolerant)
JSN-SR04T VCC 5V (BEC) Do NOT use Arduino 5V pin
All Servos VCC (Red) 5V (BEC) MG996R stalls at 2.5A
All Components GND (Brown/Black) GND (Common) BEC GND must tie to Mega GND
⚠️ Power Isolation Warning: The MG996R servos can draw up to 2.5A each during stall or rapid direction changes. If you power them from the Arduino Mega's onboard 5V regulator, you will trigger a thermal shutdown or cause a logic brownout, resulting in corrupted serial data and erratic scanning. Always use a dedicated 5V 3A BEC, and ensure the BEC ground is bonded directly to the Arduino's GND pin to establish a common reference voltage.

Step-by-Step Assembly & Power Isolation

  1. Mount the Gimbal: Assemble the pan-tilt bracket. Ensure the X-axis (pan) servo is at the base and the Y-axis (tilt) servo is on the moving carriage. Use the provided rubber dampers to reduce high-frequency vibration transfer to the sensor.
  2. Attach the Transducer: Thread the JSN-SR04T waterproof probe into the top bracket. Do not overtighten the plastic nut, as cracking the housing will ruin the acoustic seal and cause internal ringing.
  3. Wire the BEC: Connect your 7.4V LiPo or 12V wall supply to the input of the 5V 3A Buck Converter. Route the 5V output to a breadboard power rail.
  4. Distribute Power: Connect the VCC (Red) wires of both servos and the JSN-SR04T to the 5V rail. Connect all GND wires to the ground rail.
  5. Bridge Grounds: Run a single 22 AWG wire from the breadboard ground rail to any GND pin on the Arduino Mega 2560. This common ground is mandatory for the PWM and Echo signals to read correctly.
  6. Connect Signals: Wire the servo signal wires to D2 and D3, and the sensor Trig/Echo to D4 and D5 as per the mapping table.

The Raster Scan Firmware (Arduino Mega)

The following C++ firmware sweeps the servos across a 40x40 grid. It triggers the ultrasonic pulse, times the echo using pulseIn() with a strict timeout to prevent the loop from hanging on missed echoes, and outputs the resulting matrix as CSV data over Serial. You can pipe this CSV directly into Python (Matplotlib) or Processing to render a 3D surface plot.

Note: This code targets the Arduino Mega 2560 Rev3 using the standard Servo.h library.

#include <Servo.h>

// --- PIN DEFINITIONS ---
const int PAN_PIN = 2;
const int TILT_PIN = 3;
const int TRIG_PIN = 4;
const int ECHO_PIN = 5;

// --- IMAGING PARAMETERS ---
const int X_STEPS = 40;       // Horizontal resolution
const int Y_STEPS = 40;       // Vertical resolution
const int PAN_MIN = 20;       // Servo angle limits (avoid wire binding)
const int PAN_MAX = 160;
const int TILT_MIN = 30;
const int TILT_MAX = 120;

// Max range 600cm. Timeout in microseconds = (600 * 2 * 10000) / 343 = ~35000us
const unsigned long ECHO_TIMEOUT = 35000; 

Servo panServo;
Servo tiltServo;

// Image buffer stored in SRAM (40 * 40 * 2 bytes = 3200 bytes)
uint16_t imageBuffer[X_STEPS][Y_STEPS];

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; } // Wait for serial port (native USB boards)
  
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  
  panServo.attach(PAN_PIN, 500, 2400);
  tiltServo.attach(TILT_PIN, 500, 2400);
  
  // Move to home position
  panServo.write(PAN_MIN);
  tiltServo.write(TILT_MIN);
  delay(1000); // Allow servos to settle and power supply to stabilize
  
  Serial.println("Arduino Ultrasonic Imaging Rig Initialized.");
  Serial.println("Format: CSV [X, Y, Distance_cm]");
}

void loop() {
  int xAngleStep = (PAN_MAX - PAN_MIN) / (X_STEPS - 1);
  int yAngleStep = (TILT_MAX - TILT_MIN) / (Y_STEPS - 1);
  
  for (int y = 0; y < Y_STEPS; y++) {
    int currentTilt = TILT_MIN + (y * yAngleStep);
    tiltServo.write(currentTilt);
    delay(150); // Wait for mechanical settling
    
    for (int x = 0; x < X_STEPS; x++) {
      int currentPan = PAN_MIN + (x * xAngleStep);
      panServo.write(currentPan);
      delay(100); // Wait for mechanical settling
      
      uint16_t distance = readUltrasonic();
      imageBuffer[x][y] = distance;
      
      // Output CSV for external plotting
      Serial.print(x);
      Serial.print(",");
      Serial.print(y);
      Serial.print(",");
      Serial.println(distance);
    }
  }
  
  Serial.println("--- SCAN COMPLETE ---");
  // Return to home and pause before next scan
  panServo.write(PAN_MIN);
  tiltServo.write(TILT_MIN);
  delay(5000); 
}

uint16_t readUltrasonic() {
  // Ensure trigger pin is low
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(5);
  
  // Send 10us pulse to trigger
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  // Read echo with timeout
  unsigned long duration = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT);
  
  if (duration == 0) {
    // Timeout or error occurred
    return 65535; // Max uint16 value to indicate out-of-range/error
  }
  
  // Calculate distance in cm (duration / 2 / 29.1)
  uint16_t distance = duration / 58.2;
  
  // Enforce JSN-SR04T 20cm blind zone limit
  if (distance < 20) {
    return 20; 
  }
  
  return distance;
}

Debugging: First Three Things to Check When It Fails

Ultrasonic imaging rigs are notoriously temperamental on the bench. If your serial output is garbage, the servos are twitching, or the image is full of noise, check these three failure modes in order.

1. Error: Serial outputs 65535 or Echo Timeout: 0cm continuously

Cause: The transducer is failing to couple acoustic energy into the air, or the echo is returning too weak to cross the LM393 comparator threshold on the sensor board.
Fix: Check the physical mounting. If the JSN-SR04T probe is screwed in too tightly, the housing warps and dampens the piezoelectric crystal. Loosen the nut by a quarter turn. Next, ensure the sensor face is perfectly clean; even a thin layer of dust or fingerprint oil on the waterproof membrane alters the acoustic impedance and kills the signal.

2. Error: Servo jitter, Arduino resets, or Brownout Reset in serial

Cause: Voltage sag on the 5V rail. When the MG996R servos reverse direction rapidly, they draw spike currents exceeding 2A. This pulls the shared ground reference up, causing the ATmega2560 to brown out or the pulseIn() timer to miscount.
Fix: Verify you are using a dedicated 5V 3A BEC. Measure the 5V rail with a multimeter while the rig is scanning. If it dips below 4.8V, add a 1000µF electrolytic capacitor across the 5V and GND rails near the servos to buffer the transient current spikes.

3. Error: Image shows 'ghost' objects or Multipath Ghosting in corners

Cause: Side-lobe reflections. The 15° beam hits a nearby wall, bounces to the target, and returns. The sensor calculates the total flight time, placing a 'ghost' pixel further away than reality.
Fix: You cannot fix this in hardware without switching to a $110 MaxBotix sensor. Instead, apply a software median filter in your Python/Processing visualization script, or wrap the sides of the ultrasonic probe in 1-inch thick acoustic foam to physically choke the side-lobes down to a tighter 8° cone.

Scaling: How to Extend or Simplify the Build

Depending on your end goal, you may need to alter the complexity of this rig.

To Simplify (Educational / Quick Demo):
Drop the Y-axis tilt servo entirely. Mount the sensor directly to the pan servo and perform a 1D horizontal sweep (a simple radar line). Change Y_STEPS to 1 in the code. This reduces SRAM usage to negligible levels, allowing you to port the project to an Arduino Uno or Nano, and eliminates the mechanical complexity of the tilt carriage.

To Extend (High-Res 3D Mapping):
Replace the MG996R servos with NEMA 17 stepper motors driven by A4988 drivers. Servos suffer from positional hysteresis and dead-bands, meaning a commanded 45° angle might actually be 44.2° or 45.8° depending on the direction of approach. Steppers provide exact, repeatable open-loop positioning. To implement this, swap the Servo.h library for AccelStepper.h, calculate steps-per-degree based on your GT2 belt/pulley ratio, and increase the grid to 100x100. You will need to output the data to an SD card module via SPI, as streaming a 10,000-pixel matrix over 115200 baud serial will cause buffer overflows and dropped bytes.

For deeper reading on acoustic beam patterns and transducer physics, refer to the MaxBotix Sonar Acoustics Guide. For exact timing mechanics of the echo pulse, consult the Arduino pulseIn() documentation.