The Verdict: Best Setup for Arduino PID Control

If you are building a closed-loop speed or position controller, the default L298N motor driver will ruin your PID tuning. The L298N drops up to 2.5V across its H-bridge and introduces severe dead-zone non-linearities that make the derivative (D) term oscillate wildly. For precision Arduino PID control under 1.5A, the concrete pick is the Pololu TB6612FNG Dual Motor Driver Carrier paired with an Arduino Nano v3 (ATmega328P) and a hall-effect quadrature encoder.

Decision Path: Choose Your Motor Driver

Application ConstraintConcrete PickWhy It Wins
Continuous current < 1.2A, need high PWM linearityPololu TB6612FNG (Item #713)Low MOSFET on-resistance (0.5V drop), 100kHz PWM support.
Continuous current 1.5A - 5A, single directionIRFZ44N MOSFET + OptocouplerHandles high amperage, minimal heat with a heatsink.
Continuous current > 5A, bidirectional (e.g., rover)BTS7960 43A H-BridgeIndustrial-grade, built-in overcurrent protection, handles 24V systems.
Controlling AC heating elements (Sous-vide/Kiln)OMRON G3NA-210B Solid State RelayZero-cross switching prevents EMI, isolates mains from logic.

Hardware Spec Sheet & Pin Mapping

This build targets the Arduino Nano v3 (16MHz ATmega328P). Do not use the Nano 33 IoT or ESP32 for this specific code without modifying the interrupt vectors and timer registers. Budget approximately $35 for the complete bill of materials.

ComponentExact Variant / ModelEst. CostRole in Loop
MicrocontrollerArduino Nano v3 (ATmega328P, 16MHz)$6.00Computes PID math, reads interrupts.
Motor DriverPololu TB6612FNG Carrier (Item #713)$6.50Amplifies PWM signal, handles motor inductive kickback.
Motor & Encoder12V 300RPM 370 Gearmotor w/ 11 PPR Encoder$14.00Actuator and feedback sensor (44 counts/rev after quadrature).
Power Supply12V 2A Switching PSU (Mean Well GST25A12)$12.00Provides clean DC; avoid unregulated wall warts.

Wiring & Pin Mapping

Use 22 AWG stranded wire for logic signals and 18 AWG for the motor power rails. Keep encoder wires away from the motor PWM lines to prevent inductive noise from triggering false interrupts.

Arduino Nano PinTB6612FNG PinEncoder / OtherFunction
D2 (INT0)-Encoder OUT AHardware interrupt for pulse counting.
D3 (INT1)-Encoder OUT BDirection detection (read inside ISR).
D5 (PWM)PWMA-Speed control (0-255 duty cycle).
D4AIN1-Direction logic (HIGH = Forward).
GNDGNDEncoder GNDCommon ground (critical for logic reference).
5VVCC (Logic)Encoder VCCPowers logic ICs and hall sensors.

The Code: Compilable PID Motor Controller

Before compiling, install the Arduino PID Library by Brett Beauregard via the Arduino IDE Library Manager. This code includes stall detection error handling and serial setpoint adjustment.

#include <PID_v1.h>

// --- PIN DEFINITIONS ---
#define ENCODER_INT_A 2
#define ENCODER_INT_B 3
#define MOTOR_PWM     5
#define MOTOR_DIR     4

// --- PID TUNING PARAMETERS (Start conservative) ---
double Kp = 2.0, Ki = 0.5, Kd = 0.1;
double Setpoint, Input, Output;

// Initialize PID object (DIRECT means Output increases as Input decreases below Setpoint)
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

volatile long encoderCount = 0;
unsigned long lastCompute = 0;
const int SAMPLE_TIME_MS = 50; // 20Hz loop rate
const int COUNTS_PER_REV = 44; // 11 PPR * 4 (quadrature)

void setup() {
  Serial.begin(115200);
  
  pinMode(ENCODER_INT_B, INPUT);
  pinMode(MOTOR_PWM, OUTPUT);
  pinMode(MOTOR_DIR, OUTPUT);
  
  digitalWrite(MOTOR_DIR, HIGH); // Set forward direction
  
  // Attach hardware interrupt to Pin 2 (FALLING edge)
  attachInterrupt(digitalPinToInterrupt(ENCODER_INT_A), isrEncoder, FALLING);
  
  Setpoint = 100.0; // Target: 100 RPM
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(0, 255);
  myPID.SetSampleTime(SAMPLE_TIME_MS);
}

void loop() {
  unsigned long now = millis();
  
  if (now - lastCompute >= SAMPLE_TIME_MS) {
    // Calculate RPM from encoder counts accumulated in the last interval
    double currentRPM = (encoderCount * 60000.0) / (COUNTS_PER_REV * SAMPLE_TIME_MS);
    Input = currentRPM;
    encoderCount = 0; // Reset for next interval
    
    // ERROR HANDLING: Stall Detection
    // If PID demands high power but motor isn't moving, cut power to prevent burnout
    if (Output > 220 && Input < 5.0 && Setpoint > 20.0) {
      Output = 0;
      myPID.SetMode(MANUAL);
      Serial.println("ERROR: Motor stall or encoder failure detected. Loop halted.");
    } else {
      myPID.Compute();
    }
    
    analogWrite(MOTOR_PWM, (int)Output);
    
    // Telemetry
    Serial.print("SP:"); Serial.print(Setpoint);
    Serial.print(" | IN:"); Serial.print(Input);
    Serial.print(" | OUT:"); Serial.println(Output);
    
    lastCompute = now;
  }
  
  handleSerialInput();
}

void isrEncoder() {
  // Read B channel to determine direction
  if (digitalRead(ENCODER_INT_B) == HIGH) {
    encoderCount++;
  } else {
    encoderCount--;
  }
}

void handleSerialInput() {
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    if (cmd.startsWith("SET:")) {
      double newSP = cmd.substring(4).toDouble();
      if (newSP >= 0 && newSP <= 300) { // Bounds checking
        Setpoint = newSP;
        if (myPID.GetMode() == MANUAL) myPID.SetMode(AUTOMATIC); // Reset from stall
        Serial.print("Setpoint updated to: "); Serial.println(Setpoint);
      } else {
        Serial.println("ERROR: Setpoint out of bounds (0-300 RPM).");
      }
    }
  }
}

Tuning the PID Loop: A Decision Path

Tuning is where most embedded projects fail. Do not guess. Use this heuristic table based on Ziegler-Nichols principles adapted for microcontrollers. Adjust one parameter at a time, testing via the Serial Monitor telemetry.

  1. Set Ki and Kd to 0. Increase Kp until the motor speed oscillates evenly around the setpoint. Note this value as Ku (Ultimate Gain).
  2. Set Kp to 0.45 * Ku. The system will likely have a steady-state error (e.g., Setpoint is 100, Input settles at 92).
  3. Increase Ki slowly. This integrates the error over time to eliminate the steady-state offset. Stop when the speed reaches the exact setpoint without overshooting by more than 5%.
  4. Increase Kd if the system reacts too slowly to load changes. Kd predicts future error. If the motor jitters at a standstill, your encoder is picking up EMI noise; lower Kd or add a 0.1µF ceramic capacitor across the encoder A/B pins.
Bench Tip: If your motor whines at a high pitch without moving, your PWM frequency is within the audible range and your Integral term is winding up. Add myPID.SetOutputLimits(20, 255); to overcome the motor's static friction dead-zone, or use the Improved Beginner PID logic to handle integral windup.

Troubleshooting: When the Loop Fails

When your Serial monitor outputs SP:100.00 | IN:0.00 | OUT:255.00 and the motor either screams without moving or stays dead still, follow these first three checks in exact order:

  1. Verify Encoder Interrupt Wiring: Pin 2 and Pin 3 are the only hardware interrupt pins on the ATmega328P. If you wired the encoder to Pin 4 or 5, attachInterrupt() will silently fail, encoderCount will remain 0, and the PID loop will instantly wind up to 255. Move the A-channel to D2.
  2. Check the Sample Time vs. Resolution Math: If your SAMPLE_TIME_MS is too short (e.g., 10ms) and your motor is moving slowly, encoderCount might be 0 or 1, resulting in massive RPM calculation jumps. Increase SAMPLE_TIME_MS to 50ms or 100ms for low-RPM applications to allow enough pulses to accumulate for a stable derivative.
  3. Confirm Directional Logic (Positive Feedback): If the motor accelerates to maximum speed instantly when the setpoint is applied, your loop is in positive feedback. Swap the two motor leads at the TB6612FNG OUT1/OUT2 terminals, or change DIRECT to REVERSE in the PID initialization.

Extending and Simplifying the Build

Once the baseline loop is stable, you can scale the project to fit your specific application constraints.

How to Extend (Add Live Wireless Tuning)

Solder an HC-05 Bluetooth module to the Nano's hardware serial pins (D0/D1) or use SoftwareSerial on D6/D7. Pair it with a smartphone serial terminal app. You can send SET:150 commands wirelessly to change speeds, or implement a TUNE:Kp,Ki,Kd serial parser to adjust gains on the fly without recompiling. This is essential for robotics applications where the physical load changes dynamically.

How to Simplify (Drop to P-Control)

If you are building a simple cooling fan controller where a 5% steady-state error is acceptable and overshoot is irrelevant, strip out the I and D terms. Set Ki = 0 and Kd = 0. This reduces the microcontroller's math overhead, eliminates integral windup risks, and allows you to drop the quadrature encoder entirely, replacing it with a simple thermistor or voltage divider if you are controlling temperature rather than speed. For basic proportional control, a single MOSFET and a 10k potentiometer for manual setpoint adjustment will replace the entire H-bridge and encoder assembly, cutting the BOM cost by 60%.