The internal feedback potentiometer in a standard 12V linear actuator provides a reliable, absolute position reading without the homing routines required by stepper motors. However, interfacing this linear actuator position sensor with a 3.3V microcontroller like the ESP32 requires more than just plugging in three wires. You must step down the 5V analog signal, calculate the physical stroke from raw ADC counts, and filter out the aggressive electromagnetic interference (EMI) generated by the actuator's DC motor brushes.
Sensing Principle and Output Types
The most common linear actuator position sensor is a multi-turn or slide potentiometer mechanically coupled to the actuator’s lead screw or worm gear. As the motor turns the drive nut, the potentiometer’s wiper travels along a resistive carbon or conductive plastic track. This creates a variable voltage divider: supplying a fixed reference voltage (typically 5V) across the outer terminals yields an analog voltage on the center wiper pin that is strictly proportional to the physical extension of the rod. Because it is an absolute resistive measurement, the sensor retains its position data even if the microcontroller loses power.
It is critical not to conflate this analog output with digital encoder outputs. While some high-end industrial actuators use Hall-effect sensors or optical encoders to output digital quadrature pulses (requiring interrupt-driven pulse counting), the standard DIY and light-industrial actuators (like those from Firgelli or Progressive Automations) output a raw analog DC voltage. This analog voltage must be read by the microcontroller’s Analog-to-Digital Converter (ADC), meaning your code will rely on voltage sampling rather than edge-triggered interrupts.
Hardware Specs and Wiring Matrix
Before writing any code, you must match the actuator's electrical characteristics to your microcontroller's GPIO limits. Most ESP32 and Arduino boards feature 3.3V logic on their ADC pins. Feeding a 5V wiper signal directly into an ESP32 GPIO will permanently damage the silicon. A voltage divider is mandatory.
| Parameter | Standard Analog Actuator (e.g., PA-14 / FA-PO) | ESP32 DevKit V1 Limits |
|---|---|---|
| Sensor Type | Internal Potentiometer (3-wire) | 12-bit SAR ADC (Pins 32-39) |
| Potentiometer Resistance | 10 kΩ (typical), 1 kΩ to 100 kΩ range | High impedance input (>1 MΩ) |
| Supply Voltage (Outer Pins) | 5.0V DC (regulated) | 3.3V output pin (max 500mA total) |
| Output Signal (Wiper) | 0.0V to 5.0V DC (Analog) | 0.0V to 3.3V absolute max |
| Linearity Tolerance | ±0.5% to ±1.0% of full stroke | ±3% ADC non-linearity at extremes |
Below is the exact wiring matrix for safely stepping down the 5V signal to a 3.0V maximum, ensuring you stay safely within the ESP32's 3.3V ADC ceiling while maximizing resolution.
| Actuator Sensor Wire | Function | Connection / Component | ESP32 Pin |
|---|---|---|---|
| Red (or +) | Pot VCC | 5V Regulated Supply (Do not use raw 12V) | N/A |
| Black (or -) | Pot GND | Common Ground (Supply + ESP32 GND) | GND |
| White/Yellow (Wiper) | Analog Out | 10 kΩ Resistor (R1) to Wiper | GPIO 34 (via R2) |
| N/A (Divider Node) | Voltage Step-Down | 15 kΩ Resistor (R2) to GND | GPIO 34 |
The Raw-to-Unit Math and Calibration
With the 10kΩ / 15kΩ voltage divider in place, the voltage reaching the ESP32 pin is 60% of the actual wiper voltage ($V_{esp} = V_{wiper} \times \frac{15}{10+15}$). Therefore, to find the true wiper voltage, we multiply the ESP32 reading by 1.6667. Using the modern ESP32 Arduino Core analogReadMilliVolts() function bypasses the need to manually calculate the 3.3V reference offset, as it uses the chip's internal eFuse calibration data.
Here is the exact mathematical pipeline to convert raw millivolts into physical millimeters:
- Read Millivolts:
int v_esp = analogReadMilliVolts(SENSOR_PIN); - Calculate True Wiper Voltage:
float v_wiper = v_esp * 1.6667;(Result is in mV) - Map to Stroke Percentage:
float stroke_pct = v_wiper / 5000.0;(Assuming a 5000mV / 5V supply) - Convert to Millimeters:
float position_mm = stroke_pct * TOTAL_STROKE_MM;
However, raw math is never enough for physical systems. Potentiometers have a "dead band" at the physical limits of the stroke where the wiper rides off the resistive track. A 150mm actuator might only output a linear voltage change between 0.25V (retracted) and 4.75V (extended). You must perform a two-point calibration to map the electrical limits to the mechanical limits.
// Calibration constants determined by physical measurement
const float V_RETRACTED_MV = 250.0; // Measured wiper voltage at 0mm
const float V_EXTENDED_MV = 4750.0; // Measured wiper voltage at 150mm
const float MAX_STROKE_MM = 150.0;
float getCalibratedPosition(int pin) {
int v_esp_mv = analogReadMilliVolts(pin);
float v_wiper_mv = v_esp_mv * 1.6667;
// Constrain to prevent negative numbers or over-travel errors
v_wiper_mv = constrain(v_wiper_mv, V_RETRACTED_MV, V_EXTENDED_MV);
// Map the calibrated voltage range to physical millimeters
float position_mm = map(v_wiper_mv, V_RETRACTED_MV, V_EXTENDED_MV, 0, MAX_STROKE_MM);
return position_mm;
}
For projects demanding higher precision than the ESP32's internal ADC can provide, especially in the non-linear zones near 0V and 3.3V, upgrading to an external 16-bit I2C ADC like the Adafruit ADS1115 is the standard engineering fix. It eliminates the ESP32's internal noise floor and provides 0.1mm resolution on standard actuators.
Defeating Motor Noise and Interference
The most common failure mode in actuator position sensing is not a broken wire, but noisy data. A DC motor is essentially a mechanical switch sparking thousands of times a second. This generates massive broadband EMI. Furthermore, if you are driving the motor with a PWM speed controller (like a BTS7960 or L298N), the high-current switching edges induce common-mode noise into the high-impedance analog wiper trace, causing the ADC reading to jitter by 10-20mm randomly.
To stabilize your readings, you must attack the interference at both the hardware and software levels.
Hardware: The RC Low-Pass Filter
Do not route the wiper signal directly to the voltage divider. Instead, insert a simple RC (Resistor-Capacitor) low-pass filter right at the ESP32 GPIO pin. By placing a 100Ω resistor in series with the signal path and a 100nF (0.1µF) ceramic capacitor from the GPIO pin to ground, you create a hardware filter with a cutoff frequency of roughly 16 kHz. This shorts the high-frequency PWM switching noise to ground before the ADC sample-and-hold circuit can capture it. Additionally, always use twisted-pair cable for the sensor wires; twisting the wiper and ground wires together rejects magnetic field coupling from the motor's power cables.
Software: Moving Average and Outlier Rejection
Even with hardware filtering, mechanical vibration can cause the wiper to bounce microscopically. A simple moving average filter smooths the data, but a standard average is easily skewed by a single massive noise spike (an outlier). Use a trimmed mean or a median filter in your loop.
const int NUM_READINGS = 10;
int readings[NUM_READINGS];
int readIndex = 0;
float getFilteredPosition(int pin) {
// Take a new reading
readings[readIndex] = analogReadMilliVolts(pin);
readIndex = (readIndex + 1) % NUM_READINGS;
// Sort a copy of the array to find the median (rejects extreme spikes)
int sorted[NUM_READINGS];
memcpy(sorted, readings, sizeof(readings));
std::sort(sorted, sorted + NUM_READINGS);
// Use the median value for the math pipeline
int median_mv = sorted[NUM_READINGS / 2];
float v_wiper_mv = median_mv * 1.6667;
v_wiper_mv = constrain(v_wiper_mv, V_RETRACTED_MV, V_EXTENDED_MV);
return map(v_wiper_mv, V_RETRACTED_MV, V_EXTENDED_MV, 0, MAX_STROKE_MM);
}
By combining a regulated 5V reference, a properly sized 10k/15k voltage divider, a hardware RC filter, and a software median filter, your linear actuator control loop will achieve smooth, repeatable positioning accurate to within 1-2mm, entirely eliminating the erratic jumping that plagues basic analogRead() implementations.






