The Sensing Principle: Ultrasonic Time-of-Flight

An ultrasonic distance sensor like the HC-SR04 or its waterproof sibling, the JSN-SR04T, operates on the time-of-flight principle. A 40kHz piezoelectric transducer emits an eight-cycle acoustic burst, then immediately switches to listening mode. The sensor measures the exact microsecond duration between the transmission and the return of the acoustic echo bouncing off a target. Because the speed of sound in air is relatively constant at a given temperature, this time delta translates directly into physical distance.

On the output side of this system, a 12V DC linear actuator provides the mechanical work. Unlike continuous rotation motors, linear actuators push or pull a load along a fixed axis, typically using an internal lead screw driven by a brushed DC motor. By pairing the ultrasonic sensor with an H-bridge motor driver and a microcontroller, we create a closed-loop system: the sensor reads the current position, the microcontroller calculates the error against a target setpoint, and the actuator extends or retracts to minimize that error.

Wiring the Sensor and Actuator Hardware

Driving a 12V linear actuator requires a robust motor driver. While hobbyists often reach for the L298N, it is a poor choice here; its bipolar junction transistor (BJT) design drops nearly 2V and overheats rapidly above 2A. A 12V linear actuator can easily draw 5A to 10A under stall conditions. We will use the BTS7960 (IBT-2 module), which uses MOSFETs and handles up to 43A peak.

Component Pinout and Supply Specifications
Component Module Pin ESP32 DevKit V1 Pin Supply Range / Notes
JSN-SR04T Sensor VCC 5V (VIN or USB) 4.8V - 5.5V DC
JSN-SR04T Sensor Trig GPIO 5 3.3V Logic Compatible
JSN-SR04T Sensor Echo GPIO 18 5V Output (Requires Divider)
BTS7960 Driver B+ (12V) 12V PSU (+) 5.5V - 27V DC
BTS7960 Driver B- (GND) 12V PSU (-) & ESP32 GND Common Ground Required
BTS7960 Driver R_PWM GPIO 16 PWM Forward Control
BTS7960 Driver L_PWM GPIO 17 PWM Reverse Control
BTS7960 Driver R_EN & L_EN 5V (Jumpered) Logic High to Enable

Wiring Steps and Safety Callouts

  1. Power Isolation: Keep the 12V actuator power supply completely separate from the ESP32's USB/5V supply, except for the common ground connection at the BTS7960 B- terminal.
  2. Fuse the Actuator: Place an inline 10A automotive blade fuse on the 12V positive feed to the BTS7960. If the actuator jams, the stall current will melt 18 AWG wire before the driver's thermal shutdown triggers.
  3. Level Shift the Echo Pin: The JSN-SR04T outputs a 5V pulse on the Echo pin. The ESP32 GPIOs are strictly 3.3V tolerant. Build a voltage divider using a 1kΩ resistor (Echo to GPIO 18) and a 2kΩ resistor (GPIO 18 to GND) to drop the 5V signal to a safe ~3.33V.
  4. Motor Decoupling: Solder a 100nF ceramic capacitor directly across the two metal terminals of the DC motor inside the actuator housing to suppress brush arcing noise.
Callout Tip: If your ESP32 brownouts or resets when the actuator changes direction, your 12V power supply is likely sagging and coupling noise back through the ground plane. Upgrade to a power supply rated for at least 150% of the actuator's stall current (e.g., a 15A supply for a 10A stall motor).

Output Signal Math: Raw Echo to Millimeters

A common beginner mistake is treating the ultrasonic sensor as an analog device. The HC-SR04 and JSN-SR04T do not output a varying voltage. The output is a digital pulse width measured in microseconds (µs). The microcontroller's internal timer measures how long the Echo pin stays HIGH.

To convert this raw time into physical distance, we use the speed of sound. At 20°C (68°F) in dry air, sound travels at approximately 343.4 meters per second, which translates to 0.3434 millimeters per microsecond. Because the acoustic burst must travel to the target and back, we divide the total time by two.

The Raw-to-Unit Formula:
Distance (mm) = (Pulse Width in µs × 0.3434) / 2
Distance (mm) = Pulse Width in µs × 0.1717

Calibration and Environmental Scaling

The 0.1717 multiplier is only accurate at 20°C. The speed of sound increases by roughly 0.606 m/s for every 1°C rise in temperature. If your sensor and actuator system operates in an unheated garage at 5°C, your distance readings will be off by nearly 3%. For precision applications, implement temperature compensation using the formula: v = 331.3 + (0.606 × Temp_C), and dynamically update your multiplier in code. According to NIST standard atmospheric data, humidity has a negligible effect (less than 0.1%) compared to temperature and barometric pressure.

Common Interference Sources

The most severe interference in this specific pairing is Electromagnetic Interference (EMI) generated by the actuator's brushed DC motor. As the motor commutates, it generates broadband high-frequency noise. If the sensor's signal wires run parallel to the actuator's power cables, this EMI can induce voltage spikes on the Echo line. The ESP32 interprets these spikes as an early returning echo, causing the system to falsely read that an obstacle is very close, triggering erratic actuator reversals. As noted in Analog Devices' guidelines on motor drive EMI, physical separation of signal and power traces, combined with local ceramic decoupling capacitors at the motor terminals, is mandatory to preserve signal integrity.

Embedded C++ Closed-Loop Control Logic

The following code is written for the ESP32 Arduino Core (v2.x/v3.x). It implements a simple proportional deadband controller. The actuator will drive forward or backward until the target distance is reached, then stop to prevent mechanical oscillation (hunting).

#include <Arduino.h>

// Pin Definitions
const int TRIG_PIN = 5;
const int ECHO_PIN = 18;
const int PWM_FWD = 16;
const int PWM_REV = 17;

// Control Parameters
const float TARGET_DIST_MM = 250.0; // Target 250mm
const float DEADBAND_MM = 15.0;     // +/- 15mm tolerance
const int MAX_PWM = 200;            // Max speed (0-255)
const int MIN_PWM = 80;             // Minimum voltage to overcome static friction

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  
  // Configure ESP32 LEDC PWM for BTS7960 (500Hz is optimal for DC motors)
  ledcAttach(PWM_FWD, 500, 8);
  ledcAttach(PWM_REV, 500, 8);
  
  // Ensure actuator is stopped on boot
  ledcWrite(PWM_FWD, 0);
  ledcWrite(PWM_REV, 0);
  
  Serial.println("Sensor and Actuator Closed-Loop System Initialized.");
}

float readDistanceMM() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  // Timeout set to 30000us (~5 meters max range)
  long duration = pulseIn(ECHO_PIN, HIGH, 30000); 
  
  if (duration == 0) return -1.0; // Timeout/Error
  
  // Raw to unit math (assuming 20C ambient)
  return duration * 0.1717; 
}

void driveActuator(int pwmValue, bool forward) {
  if (forward) {
    ledcWrite(PWM_REV, 0);
    ledcWrite(PWM_FWD, constrain(pwmValue, 0, 255));
  } else {
    ledcWrite(PWM_FWD, 0);
    ledcWrite(PWM_REV, constrain(pwmValue, 0, 255));
  }
}

void loop() {
  float currentDist = readDistanceMM();
  
  if (currentDist < 0) {
    Serial.println("Error: Sensor timeout or EMI spike.");
    driveActuator(0, true); // Stop on error
    delay(100);
    return;
  }
  
  float error = currentDist - TARGET_DIST_MM;
  
  // Check if we are inside the acceptable deadband
  if (abs(error) <= DEADBAND_MM) {
    driveActuator(0, true); // Stop
  } 
  else if (error > DEADBAND_MM) {
    // Too far, need to extend (assuming extending reduces distance to target)
    int pwm = map(abs(error), DEADBAND_MM, 500, MIN_PWM, MAX_PWM);
    driveActuator(pwm, true);
  } 
  else {
    // Too close, need to retract
    int pwm = map(abs(error), DEADBAND_MM, 500, MIN_PWM, MAX_PWM);
    driveActuator(pwm, false);
  }
  
  Serial.printf("Dist: %.1f mm | Error: %.1f mm\n", currentDist, error);
  delay(50); // 20Hz control loop rate
}

Sensor and Actuator Interfacing FAQ

Why does my sensor and actuator system jitter when the motor runs?

Jitter is almost always caused by Electromagnetic Interference (EMI) from the actuator's brushed DC motor or voltage sag on the 5V logic rail. When the motor switches direction, the inductive kickback generates high-frequency noise that couples into the ultrasonic sensor's Echo pin. The ESP32 reads this noise as a premature echo, resulting in a falsely short distance reading. The microcontroller then panics and reverses the motor, creating a feedback loop of jitter. Fix this by soldering a 100nF ceramic capacitor directly across the motor terminals, routing sensor wires away from power wires, and ensuring your 5V logic supply is decoupled with a bulk electrolytic capacitor (e.g., 470µF) near the sensor.

How do I calibrate a sensor and actuator pair for sub-millimeter accuracy?

You cannot achieve reliable sub-millimeter accuracy with a 40kHz ultrasonic sensor. The wavelength of 40kHz sound in air is roughly 8.5mm, meaning the acoustic burst itself is physically larger than the precision you are trying to measure. Furthermore, temperature gradients and acoustic scattering off the actuator's lead screw will introduce noise. If your application requires 0.1mm to 1.0mm precision for closed-loop positioning, you must abandon ultrasonics and switch to a Time-of-Flight (ToF) laser sensor like the VL53L1X, or mount a physical linear potentiometer directly to the actuator shaft for analog position feedback.

What is the best microcontroller for a high-speed sensor and actuator loop?

For high-speed closed-loop control (e.g., balancing robots or fast-acting pneumatic valves), the polling rate of the sensor is the bottleneck. The standard Arduino Uno (ATmega328P) is limited by its slow pulseIn() blocking function and lack of hardware PWM resolution. The ESP32 is vastly superior here because its dual-core 240MHz architecture allows you to run the ultrasonic ping on Core 0 while executing the PID control math and PWM updates on Core 1. For even higher speeds where ultrasonics are too slow (20Hz max ping rate), makers should look at the Teensy 4.1 paired with an optical encoder, which can execute control loops at 10kHz or higher.