Quadrature encoder feedback combined with a PID (Proportional-Integral-Derivative) controller is a closed-loop embedded system that continuously measures a DC motor's actual rotational speed and dynamically adjusts the PWM duty cycle to match a target velocity, ensuring a DIY robot vacuum drives straight and navigates accurately. Without this, your robot is just a blind chassis that will spiral into walls the moment one wheel hits a thicker patch of carpet. This concept changes a basic open-loop motor circuit—where a microcontroller blindly outputs a fixed voltage percentage—into an active, self-correcting motion system that compensates in real-time for battery voltage sag, varying floor friction, and mechanical drag. Builders commonly confuse open-loop PWM speed control (simply guessing a 60% duty cycle means 60% speed) with true closed-loop PID velocity control, or they mistakenly buy expensive absolute encoders when high-resolution incremental quadrature encoders are the correct, cost-effective choice for continuous wheel rotation.

Spec Sheet: Choosing Encoders and Motor Drivers

Before writing a single line of C++ or MicroPython, you must select hardware that physically supports closed-loop control. A DIY robot vacuum typically requires a differential drive setup (two independently driven wheels plus a passive caster). The motor driver must support bidirectional PWM and logic-level compatibility with your microcontroller, while the encoders must provide enough Counts Per Revolution (CPR) to allow the PID loop to make micro-adjustments at low speeds without stalling.

Voltage Sag Warning: As your robot vacuum's LiFePO4 or Li-ion battery pack depletes from 12.6V down to 10.5V, an open-loop PWM value of 180 (out of 255) will yield progressively slower wheel speeds. Closed-loop PID automatically increases the PWM duty cycle to maintain the target RPM despite the dropping battery voltage.
Component Type Model / Part Number Operating Voltage Resolution (CPR) Max Continuous Current Typical Cost
N20 Gearmotor w/ Encoder JGA25-370 (12V variant) 6-12V DC 4400 CPR (quadrature) 1.2A $14.50
37mm Planetary Gearmotor DFRobot FIT0533 12V DC 4000 CPR 2.5A $38.00
Dual Motor Driver TB6612FNG 2.5-13.5V (VM) N/A 1.2A per channel $4.50
High-Power Motor Driver DRV8701 (Pololu board) 5.5-45V N/A 12A continuous $18.00
Wheel Encoder (Retrofit) Pololu 20 CPR Magnetic 3.3-5V 20 CPR (80 quadrature) N/A $6.00

For a standard 250mm diameter DIY robot vacuum, the JGA25-370 motors paired with a TB6612FNG driver offer the best balance of torque, resolution, and cost. The 4400 CPR provides immense granularity, allowing the ESP32 to detect wheel slip within milliseconds. If you are building a heavier, multi-level mapping vacuum with large drive wheels, step up to the 37mm planetary motors and the DRV8701 driver to handle the >5A stall currents without triggering thermal shutdown.

The Math in Motion: A Worked PID Numeric Example

To understand what the microcontroller is actually doing under the hood, let us walk through a single discrete PID calculation cycle. Assume we are using an ESP32 DevKit v1 running a control loop at 50 Hz (a 20-millisecond sampling interval). Our target velocity is 100 RPM, and our encoders output 4000 Counts Per Revolution (CPR) using 4x quadrature decoding.

First, we calculate the target counts per 20ms interval:
Target Counts = (100 rev/min) * (4000 counts/rev) / (60 sec/min) * (0.02 sec/interval) = 133.33 counts

During the last 20ms interval, the left wheel hit a high-pile carpet. The encoder only registered 110 actual counts. The wheel is dragging.

  • Error (e): 133.33 (target) - 110 (actual) = 23.33
  • Previous Error (e_prev): 15.0 (from the prior 20ms interval)
  • Accumulated Integral (I_sum): 12.0 (running total from previous cycles)

Now, we apply our tuned PID constants for this specific chassis mass and wheel friction: Kp = 1.5, Ki = 0.8, Kd = 0.2.

  1. Proportional Term (P): Reacts to the current error.
    P = Kp * e = 1.5 * 23.33 = 34.99
  2. Integral Term (I): Reacts to accumulated past error (eliminates steady-state drag).
    I_new = I_sum + (Ki * e) = 12.0 + (0.8 * 23.33) = 12.0 + 18.66 = 30.66
  3. Derivative Term (D): Reacts to the rate of change of error (prevents overshooting when the wheel suddenly breaks free of the carpet).
    D = Kd * (e - e_prev) = 0.2 * (23.33 - 15.0) = 0.2 * 8.33 = 1.66

Total PID Output: 34.99 + 30.66 + 1.66 = 67.31

If the baseline PWM duty cycle sent to the TB6612FNG was 150 (out of 255), the new commanded PWM becomes:
New PWM = 150 + 67.31 = 217.31 (rounded to 217).

The ESP32 immediately updates the hardware PWM register via ledcWrite(). The motor driver pushes more current to the left motor, torque increases, and the wheel breaks through the carpet friction, returning to the target 133 counts per interval. For a deeper theoretical breakdown of these constants, refer to this comprehensive guide on understanding PID controllers from All About Circuits.

Where You Meet This in Practice: ESP32 Wiring and PCNT

Theory is useless if your microcontroller drops encoder pulses. This is the most common failure mode in DIY robot vacuum builds. At 100 RPM with a 4400 CPR encoder, the microcontroller must process 7,333 interrupts per second, per wheel. If you use standard software interrupts (like Arduino's attachInterrupt()) on an ESP32 while simultaneously running Wi-Fi, MQTT telemetry, and LiDAR SLAM mapping, the CPU will bottleneck, drop counts, and your PID loop will receive false velocity data, causing the robot to violently overcorrect and spin in circles.

The Hardware Fix: Never use software interrupts for high-CPR encoders on the ESP32. Instead, use the ESP32's built-in Pulse Counter (PCNT) peripheral. The PCNT counts encoder edges in dedicated hardware silicon, completely independent of the main CPU cores and Wi-Fi interrupts. You simply read the accumulated count register every 20ms and reset it. Consult the official Espressif PCNT API documentation for implementation details.

Wiring the TB6612FNG to the ESP32:

  • VM (Motor Power): Connect to your 12V battery pack positive terminal (ensure a 30A BMS is in place for short-circuit protection).
  • VCC (Logic Power): Connect to the ESP32's 3.3V output. Do not connect this to 5V, or you will fry the ESP32's GPIO pins when the driver sends logic HIGH signals back to the MCU.
  • PWMA / PWMB: Connect to ESP32 pins capable of hardware PWM (e.g., GPIO 16 and GPIO 17).
  • STBY (Standby): Tie directly to 3.3V to keep the driver permanently active, or route to a GPIO if you want software-controlled sleep modes to save battery during docking.
  • Encoder A/B Phases: Route to GPIOs assigned to the PCNT peripheral. Ensure you use 10kΩ pull-up resistors to 3.3V if your specific encoder board lacks them, as floating encoder lines will introduce phantom counts from EMI generated by the brushed DC motors.

When tuning the PID constants on the physical chassis, use the Ziegler-Nichols heuristic method. Set Ki and Kd to zero. Slowly increase Kp until the wheels oscillate (speed up and slow down rhythmically) while the robot is lifted off the ground. Note this critical Kp value, then set your final Kp to roughly 60% of that critical value. Slowly introduce Ki to eliminate the steady-state error (the difference between target and actual RPM when pushing against a wall), and finally add a small Kd to dampen the oscillations when the robot transitions from hard floor to carpet.

Frequently Asked Questions

Why does my robot vacuum still drift in a curve even with perfect PID tuning on both wheels?
PID velocity control only ensures both wheels spin at the exact same RPM. It does not account for physical realities like a 2mm difference in wheel diameter, tire tread wear, or wheel slip on polished hardwood. To drive perfectly straight over long distances, you must fuse your wheel encoder data with an IMU (Inertial Measurement Unit, like the BNO085) using a Kalman or Madgwick filter to correct for yaw drift.

Can I just measure the motor's back-EMF voltage to determine speed instead of buying encoders?
While theoretically possible, back-EMF measurement on a PWM-driven brushed motor is incredibly noisy and requires complex analog filtering and precise ADC timing during the PWM 'off' cycles. For a DIY robot vacuum requiring millimeter-precision for mapping, quadrature encoders are mandatory. The SparkFun TB6612FNG hookup guide provides excellent reference schematics for integrating these cleanly.

What happens if an encoder wire breaks while the vacuum is running?
The actual counts will drop to zero. The PID Integral (I) term will wind up to its maximum limit trying to reach the target speed, and the microcontroller will command 100% PWM to the motor driver. The wheel will spin at maximum uncontrolled speed. Your firmware must include a 'sanity check' watchdog: if PWM exceeds 220 but encoder counts remain below 10 for three consecutive cycles, trigger an emergency stop and flash an error code.