The most reliable setup for a hobbyist closed-loop arduino pid controller targeting DC motor speed is an Arduino Uno R3 paired with the PID_v1 library, a VNH5019 dual motor driver, and a 64 CPR quadrature encoder, running at a strict 10ms sample time. Unlike open-loop PWM control, which sags under mechanical load, a PID (Proportional-Integral-Derivative) loop constantly measures the actual shaft speed via the encoder and adjusts the PWM duty cycle to maintain your target RPM.
This guide gives you the exact hardware, the pin mapping, the compilable C++ code with atomic interrupt handling, and the debugging decision tree to fix the oscillation and compilation errors that plague 90% of first-time PID builds.
The Decision Path: Choosing Your PID Architecture
Not all PID loops are created equal. The sample time and sensor type dictate which library configuration you need. Use this decision table to lock in your architecture before buying parts.
| Application | Sensor Type | Sample Time | Library / Mode | Verdict |
|---|---|---|---|---|
| Sous-Vide / Thermal Chamber | Thermocouple (MAX31855) | 1000ms - 5000ms | PID_v1 (DIRECT) | Choose for high-inertia thermal systems. |
| 3D Printer Heated Bed | Thermistor (100k) | 500ms | PID_v1 (DIRECT) | Choose for standard Marlin-style heating. |
| DC Motor Speed Control | Quadrature Encoder | 10ms - 50ms | PID_v1 (DIRECT) | DEFAULT PICK: Fast response, handles load spikes. |
| Stepper Motor Position | Limit Switches / Encoders | 1ms | PID_v1 (REVERSE) | Choose for CNC/robot arm joint positioning. |
Hardware Spec Sheet & Pin Mapping
You need components that can handle the electrical noise of a brushed DC motor while providing clean logic-level signals back to the ATmega328P microcontroller. Expect to spend roughly $75-$90 on these specific bench-tested parts.
| Component | Exact Variant / Model | Approx. Price | Why This Part? |
|---|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P DIP) | $27.00 | Hardware interrupts on D2/D3 are stable for encoder reading. |
| Motor Driver | Pololu VNH5019 Dual Motor Driver | $32.00 | Handles up to 12A continuous, 5V logic compatible, built-in flyback diodes. |
| Encoder | SparkFun Quadrature Encoder (64 CPR) | $15.00 | 64 Counts Per Rev (256 with 4x decoding) provides enough resolution at low RPM. |
| Motor | 12V DC Planetary Gearmotor (100 RPM) | $18.00 | Low base RPM makes PID tuning manageable for beginners. |
Pin Mapping Table
Wire the system exactly as shown. Do not move the encoder pins; D2 and D3 are the only pins on the Uno R3 that support hardware interrupts required for reliable quadrature decoding.
| Arduino Uno R3 Pin | Destination Module | Module Pin | Notes |
|---|---|---|---|
| D2 (INT0) | Encoder | OUT A | Must use hardware interrupt. |
| D3 (INT1) | Encoder | OUT B | Read in ISR for direction. |
| D9 (OC1A) | VNH5019 Driver | PWM 1A | Hardware PWM pin. |
| D7 | VNH5019 Driver | DIR 1A | Digital HIGH/LOW for direction. |
| 5V | Encoder / Driver | VCC / VDD | Logic power only. |
| GND | All Modules | GND | Ensure common ground with 12V supply. |
Complete PID Motor Control Code (Arduino Uno R3)
This code targets the Arduino Uno R3 (ATmega328P) and requires the Arduino PID Library (PID_v1) installed via the Library Manager. It includes atomic interrupt reading to prevent race conditions and a safety shutoff if the encoder stops reading.
#include <PID_v1.h>
// --- PIN DEFINITIONS ---
const int ENCODER_PIN_A = 2; // Hardware interrupt 0
const int ENCODER_PIN_B = 3; // Hardware interrupt 1
const int MOTOR_PWM_PIN = 9; // Hardware PWM
const int MOTOR_DIR_PIN = 7; // Direction control
// --- PID VARIABLES ---
double Setpoint, Input, Output;
// Initial tuning parameters (conservative start)
double Kp = 2.5, Ki = 4.0, Kd = 0.2;
// PID Object - MUST pass variables by reference (pointers)
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
// --- ENCODER & TIMING VARIABLES ---
volatile long encoderCount = 0;
unsigned long lastSampleTime = 0;
const int SAMPLE_TIME_MS = 10; // 10ms loop = 100Hz update rate
const int ENCODER_CPR = 256; // 64 physical CPR * 4x quadrature decoding
const double MAX_SAFE_RPM = 150.0;
void setup() {
Serial.begin(115200);
// Enable internal pull-ups to prevent floating encoder noise
pinMode(ENCODER_PIN_A, INPUT_PULLUP);
pinMode(ENCODER_PIN_B, INPUT_PULLUP);
pinMode(MOTOR_PWM_PIN, OUTPUT);
pinMode(MOTOR_DIR_PIN, OUTPUT);
// Attach interrupt to Pin A, trigger on RISING edge
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_A), readEncoder, RISING);
// Target speed in RPM
Setpoint = 60.0;
myPID.SetMode(AUTOMATIC);
myPID.SetSampleTime(SAMPLE_TIME_MS);
myPID.SetOutputLimits(0, 255); // 8-bit PWM limit
digitalWrite(MOTOR_DIR_PIN, HIGH); // Set forward direction
}
void loop() {
unsigned long now = millis();
if (now - lastSampleTime >= SAMPLE_TIME_MS) {
lastSampleTime = now;
// 1. Atomic read of encoder count to prevent ISR race conditions
noInterrupts();
long count = encoderCount;
encoderCount = 0;
interrupts();
// 2. Calculate RPM
// RPM = (counts / counts_per_rev) * (60_sec / sample_time_sec)
Input = (count * 60000.0) / (ENCODER_CPR * SAMPLE_TIME_MS);
// 3. Safety Error Handling: Runaway motor or disconnected encoder
if (Input > MAX_SAFE_RPM && Output > 200) {
myPID.SetMode(MANUAL);
analogWrite(MOTOR_PWM_PIN, 0);
Serial.println("ERROR: RPM exceeded safe limit or encoder disconnected. System halted.");
while(1); // Halt execution
}
// 4. Compute PID and write to motor
myPID.Compute();
analogWrite(MOTOR_PWM_PIN, Output);
// Debug output (throttle to every 100ms to avoid serial buffer flooding)
if (count % 10 == 0) {
Serial.print("RPM: "); Serial.print(Input);
Serial.print(" | PWM: "); Serial.println(Output);
}
}
}
// --- INTERRUPT SERVICE ROUTINE ---
void readEncoder() {
// Read Pin B to determine direction
if (digitalRead(ENCODER_PIN_B) == HIGH) {
encoderCount++;
} else {
encoderCount--;
}
}
Debugging: Fixing Compilation Errors and Motor Oscillation
The PID_v1 library is notorious for throwing specific compiler errors if the constructor is misconfigured, and the hardware will oscillate violently if the timing is off. Here is how to fix the most common bench failures.
Exact Compilation Errors
Error 1: error: 'PID' does not name a type
- Cause A (Most Likely): You forgot
#include <PID_v1.h>at the very top of the sketch. - Cause B: The library is not installed. Go to Sketch > Include Library > Manage Libraries, search for "PID by Brett Beauregard", and install version 1.2.1.
Error 2: error: no matching function for call to 'PID::PID(double, double, double, double, double, double, int)'
- Cause A (Most Likely): You passed the variables by value instead of by reference. The PID library requires pointers so it can read and write to the variables in the background. You must use the address-of operator:
&Input, &Output, &Setpoint. See the code block above for the correct syntax.
The First 3 Things to Check When the Motor Oscillates or Fails
- Verify Sample Time Consistency: The
myPID.Compute()function relies on being called at exact, regular intervals. If you havedelay()statements or longSerial.print()blocking yourloop(), the PID math will break down and cause overshoot. Always use themillis()non-blocking pattern shown in the code. - Check Encoder Pull-ups and Noise: Brushed DC motors generate massive EMI (Electromagnetic Interference). If your encoder wires are unshielded and run parallel to the motor power wires, the ATmega328P will register phantom encoder ticks. Ensure
INPUT_PULLUPis set, and physically separate signal wires from the 12V motor lines. - Eliminate Derivative Kick: If you change the
Setpointvariable on the fly (e.g., via a potentiometer or serial command), the Derivative term will see a massive instantaneous spike and slam the motor. To fix this, usemyPID.SetControllerDirection(REVERSE)logic, or better yet, leave the Setpoint alone and tune the system for a fixed RPM.
Tuning Kp, Ki, Kd and Extending the Build
Out of the box, the Kp=2.5, Ki=4.0, Kd=0.2 values will get the motor moving, but it will likely be sluggish or slightly unstable. You must tune the constants for your specific mechanical load. Refer to the SparkFun Encoder Tutorial for deeper quadrature theory if your counts seem erratic.
The Tuning Decision Tree
| Symptom on the Bench | What is Happening | Action to Take |
|---|---|---|
| Motor never reaches target RPM, stalls short. | Proportional term is too weak to overcome static friction. | Increase Kp by 50% until it reaches the target. |
| Motor reaches RPM but constantly hunts (oscillates +/- 10 RPM). | Integral windup or Proportional term is too aggressive. | Decrease Kp slightly, Increase Kd to add damping. |
| Motor takes 5+ seconds to settle at the target RPM. | System is overdamped; Integral term isn't accumulating fast enough. | Increase Ki to eliminate steady-state error faster. |
| Motor violently jerks or screeches when starting. | Derivative kick or sample time is too fast for the mechanical inertia. | Set Kd = 0. Increase SAMPLE_TIME_MS to 50ms. |
How to Extend the Build
Once the baseline PID loop is stable, you can improve performance by adding a software low-pass filter to the encoder input. Mechanical vibration causes micro-fluctuations in the RPM reading, which the Derivative term amplifies. Add this single line right before myPID.Compute():
Input = (0.8 * Input) + (0.2 * lastInput);
lastInput = Input; // Declare lastInput globally
This exponential moving average smooths out high-frequency noise without introducing the severe phase lag of a hardware capacitor filter.
How to Simplify the Build
If you do not have a quadrature encoder and just need a motor to spin at a roughly consistent rate based on a potentiometer dial, drop the PID library entirely. Use a simplified open-loop mapping approach: read the potentiometer on A0 (0-1023), map it to 0-255, and write it to the PWM pin. It will not compensate for load changes (e.g., the motor slowing down when you press on the shaft), but it eliminates the need for interrupts, atomic reads, and PID tuning, reducing the code to under 15 lines.
Ki) can cause the motor to suddenly reverse direction or spin to maximum RPM to clear an accumulated error state.






