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 Constraint | Concrete Pick | Why It Wins |
|---|---|---|
| Continuous current < 1.2A, need high PWM linearity | Pololu TB6612FNG (Item #713) | Low MOSFET on-resistance (0.5V drop), 100kHz PWM support. |
| Continuous current 1.5A - 5A, single direction | IRFZ44N MOSFET + Optocoupler | Handles high amperage, minimal heat with a heatsink. |
| Continuous current > 5A, bidirectional (e.g., rover) | BTS7960 43A H-Bridge | Industrial-grade, built-in overcurrent protection, handles 24V systems. |
| Controlling AC heating elements (Sous-vide/Kiln) | OMRON G3NA-210B Solid State Relay | Zero-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.
| Component | Exact Variant / Model | Est. Cost | Role in Loop |
|---|---|---|---|
| Microcontroller | Arduino Nano v3 (ATmega328P, 16MHz) | $6.00 | Computes PID math, reads interrupts. |
| Motor Driver | Pololu TB6612FNG Carrier (Item #713) | $6.50 | Amplifies PWM signal, handles motor inductive kickback. |
| Motor & Encoder | 12V 300RPM 370 Gearmotor w/ 11 PPR Encoder | $14.00 | Actuator and feedback sensor (44 counts/rev after quadrature). |
| Power Supply | 12V 2A Switching PSU (Mean Well GST25A12) | $12.00 | Provides 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 Pin | TB6612FNG Pin | Encoder / Other | Function |
|---|---|---|---|
| D2 (INT0) | - | Encoder OUT A | Hardware interrupt for pulse counting. |
| D3 (INT1) | - | Encoder OUT B | Direction detection (read inside ISR). |
| D5 (PWM) | PWMA | - | Speed control (0-255 duty cycle). |
| D4 | AIN1 | - | Direction logic (HIGH = Forward). |
| GND | GND | Encoder GND | Common ground (critical for logic reference). |
| 5V | VCC (Logic) | Encoder VCC | Powers 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.
- Set Ki and Kd to 0. Increase Kp until the motor speed oscillates evenly around the setpoint. Note this value as Ku (Ultimate Gain).
- Set Kp to 0.45 * Ku. The system will likely have a steady-state error (e.g., Setpoint is 100, Input settles at 92).
- 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%.
- 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.
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:
- 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,encoderCountwill remain 0, and the PID loop will instantly wind up to 255. Move the A-channel to D2. - Check the Sample Time vs. Resolution Math: If your SAMPLE_TIME_MS is too short (e.g., 10ms) and your motor is moving slowly,
encoderCountmight be 0 or 1, resulting in massive RPM calculation jumps. IncreaseSAMPLE_TIME_MSto 50ms or 100ms for low-RPM applications to allow enough pulses to accumulate for a stable derivative. - 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
DIRECTtoREVERSEin 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%.






