An Arduino PID controller is one of the most powerful techniques you can add to your embedded projects. Whether you need rock-solid motor speed regulation or precise temperature control, a properly tuned PID loop eliminates the hunting and overshoot that simple on/off or proportional-only control inevitably produces. In this comprehensive tutorial, we break down the PID algorithm from first principles, show you exactly how to implement it on Arduino hardware, and walk through two complete real-world examples — DC motor speed control and a temperature regulation system.
Table of Contents
- What Is a PID Controller?
- The PID Equation Explained
- Using the Arduino PID Library
- Project 1 – DC Motor Speed Control
- Project 2 – Temperature PID Control
- PID Tuning: Ziegler–Nichols and Manual Methods
- Advanced Tips: Anti-Windup, Derivative Filter
- Frequently Asked Questions
What Is a PID Controller?
PID stands for Proportional, Integral, Derivative. It is a closed-loop feedback control algorithm that continuously calculates an error value — the difference between a desired setpoint (SP) and a measured process variable (PV) — and applies a correction based on three terms:
- P (Proportional): Responds to the current error. Larger error → larger correction.
- I (Integral): Responds to accumulated past error. Eliminates steady-state offset that pure P-control leaves behind.
- D (Derivative): Responds to the rate of change of error. Acts as a damper, preventing overshoot.
PID controllers are everywhere in the real world: industrial furnaces, automotive cruise control, quadcopter flight stabilisation, CNC machine axes, and home thermostats. On Arduino, you can implement a surprisingly capable PID loop using even the basic Uno or Nano.
The PID Equation Explained
The continuous-time PID equation is:
u(t) = Kp·e(t) + Ki·∫e(t)dt + Kd·(de/dt)
Where:
u(t)— controller output (e.g., PWM duty cycle 0–255)e(t)— error = setpoint − measured valueKp,Ki,Kd— tuning gains
For Arduino (discrete time with sample interval dt), the equation becomes:
error = setpoint - input;
integral += error * dt;
derivative = (error - prevError) / dt;
output = Kp*error + Ki*integral + Kd*derivative;
prevError = error;
The sample interval dt must be consistent. Using millis() to track time and only running the PID calculation every fixed interval (e.g., 10 ms) is the correct approach — never poll inside a delay().
Using the Arduino PID Library
Brett Beauregard’s open-source PID_v1 library is the standard choice for Arduino PID projects. Install it via the Arduino IDE Library Manager (search “PID by Brett Beauregard”).
Basic setup:
#include <PID_v1.h>
double Setpoint, Input, Output;
double Kp = 2.0, Ki = 5.0, Kd = 1.0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
void setup() {
myPID.SetMode(AUTOMATIC);
myPID.SetOutputLimits(0, 255); // for PWM
myPID.SetSampleTime(10); // ms
}
void loop() {
Input = readSensor(); // your sensor read
myPID.Compute(); // run PID
analogWrite(PWM_PIN, Output); // apply output
}
The library handles sample timing internally — it will only recompute when the sample time has elapsed, so you can safely call Compute() every loop iteration.
Project 1 – DC Motor Speed Control
In this project we use an encoder-equipped DC motor, an L298N or L293D motor driver, and a rotary encoder to close the speed loop.
Hardware Required
- Arduino Uno or Mega
- DC gear motor with hall-effect encoder (or optical encoder disc)
- L298N motor driver module
- 12 V power supply
- Jumper wires and breadboard
Wiring Overview
- Encoder channel A → Arduino interrupt pin 2
- Encoder channel B → Arduino pin 3
- L298N IN1/IN2 → Arduino digital pins 7, 8
- L298N ENA → Arduino PWM pin 9
Complete Sketch
#include <PID_v1.h>
const int ENC_A = 2;
const int ENC_B = 3;
const int PWM_PIN = 9;
const int IN1 = 7, IN2 = 8;
volatile long encoderCount = 0;
unsigned long lastTime = 0;
long lastCount = 0;
double Setpoint = 200.0; // target RPM
double Input, Output;
double Kp = 1.2, Ki = 0.8, Kd = 0.05;
PID motorPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
void encoderISR() {
if (digitalRead(ENC_B)) encoderCount++;
else encoderCount--;
}
void setup() {
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
pinMode(PWM_PIN, OUTPUT);
digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); // forward
attachInterrupt(digitalPinToInterrupt(ENC_A), encoderISR, RISING);
motorPID.SetMode(AUTOMATIC);
motorPID.SetOutputLimits(0, 255);
motorPID.SetSampleTime(50);
Serial.begin(115200);
}
void loop() {
unsigned long now = millis();
if (now - lastTime >= 50) {
long count = encoderCount;
long delta = count - lastCount;
// Assuming 20 pulses per revolution
Input = (delta / 20.0) * (1000.0 / 50.0) * 60.0; // RPM
lastCount = count;
lastTime = now;
}
motorPID.Compute();
analogWrite(PWM_PIN, (int)Output);
Serial.print("SP:"); Serial.print(Setpoint);
Serial.print(" RPM:"); Serial.println(Input);
}
Start with low Kp (0.5–1.5) and zero Ki, Kd. Once the motor roughly tracks the setpoint, increase Ki to eliminate steady-state error. Add a small Kd only if you see oscillation.
Project 2 – Temperature PID Control
A temperature controller is perhaps the most common PID application for makers — used in reflow ovens, sous-vide cookers, reptile enclosures, and fermentation chambers.
Hardware Required
- Arduino Uno
- DS18B20 or LM35 temperature sensor
- Solid-state relay (SSR) or MOSFET module (for heater switching)
- Resistive heating element (nichrome wire, ceramic heater, etc.)
- 5 V power supply for Arduino
Complete Temperature PID Sketch
#include <PID_v1.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_BUS 4
#define SSR_PIN 9
#define WINDOW_MS 5000UL // time-proportioning window
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
double Setpoint = 60.0; // °C target
double Input, Output;
double Kp = 5.0, Ki = 0.1, Kd = 1.0;
PID tempPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
unsigned long windowStart;
void setup() {
sensors.begin();
pinMode(SSR_PIN, OUTPUT);
tempPID.SetMode(AUTOMATIC);
tempPID.SetOutputLimits(0, WINDOW_MS);
tempPID.SetSampleTime(1000);
windowStart = millis();
Serial.begin(115200);
}
void loop() {
sensors.requestTemperatures();
Input = sensors.getTempCByIndex(0);
tempPID.Compute();
// Time-proportioning output
if (millis() - windowStart > WINDOW_MS) windowStart += WINDOW_MS;
digitalWrite(SSR_PIN, (millis() - windowStart < Output) ? HIGH : LOW);
Serial.print("Temp:"); Serial.print(Input);
Serial.print(" Output:"); Serial.println(Output);
}
Time-proportioning (instead of simple PWM) is used for heaters because SSRs and heating elements respond slowly — the PID output becomes the ON-time within a 5-second window.
PID Tuning: Ziegler–Nichols and Manual Methods
Getting gains right is 80% of successful PID implementation. Two popular approaches:
Manual Tuning (Recommended for Beginners)
- Set
Ki = 0,Kd = 0. IncreaseKpuntil the output oscillates steadily around the setpoint. - Note the oscillation period
Tu. HalveKp(this is the ultimate gain divided by 2). - Set
Ki = Kp / Tu. The system should now reach setpoint without steady-state error. - Add
Kd = Kp * Tu / 8only if there is overshoot.
Ziegler–Nichols Closed-Loop Method
- Find ultimate gain
Ku(value of Kp at which sustained oscillations appear). - Measure oscillation period
Tu. - Apply:
Kp = 0.6·Ku,Ki = 1.2·Ku/Tu,Kd = 0.075·Ku·Tu.
For temperature systems, start with Kp 2–10, Ki 0.05–0.5, Kd 0.5–5. For motor speed, faster dynamics demand higher Ki and lower Kd.
Advanced Tips: Anti-Windup, Derivative Filter
Integral Windup
If the output saturates (motor at full speed but can’t reach setpoint), the integral term keeps accumulating — when the system finally responds, it massively overshoots. Prevention strategies:
- Output clamping: The PID library’s
SetOutputLimits()automatically clamps the integral when output saturates. - Conditional integration: Only integrate when output is within limits.
- Back-calculation: Subtract the saturation excess from the integral each cycle.
Derivative Kick
A sudden setpoint change causes a derivative spike (“D kick”). Fix: differentiate the measurement, not the error — PID_v1 does this by default when you use DIRECT mode without setpoint changes.
Derivative Filter
Noisy sensors make the D term amplify noise into wild output swings. Add a low-pass filter:
float alpha = 0.1; // 0–1, smaller = smoother
dFiltered = alpha * (error - prevError) / dt + (1 - alpha) * dFiltered;
Sample Rate Selection
Rule of thumb: sample at 10–20× the system’s natural frequency. Slow thermal systems → 500 ms to 2 s. Fast motor loops → 5–20 ms. The Arduino Uno’s ADC conversion takes ~100 µs by default (see our fast analog read tutorial for 10× speedup).
Frequently Asked Questions
What is the best Arduino board for PID motor control?
The Arduino Uno R3 is sufficient for single-motor PID loops. For multi-axis control (e.g., CNC, 3D printer, robot arm), use the Arduino Mega 2560 which provides 15 PWM pins, more timers, and 8 KB SRAM to handle multiple concurrent PID instances without running out of memory.
How do I know if my PID is oscillating due to Kp or Kd?
If the system oscillates at a frequency close to your sample rate, it is likely derivative gain (Kd) amplifying noise — reduce Kd or add a derivative filter. If oscillations are slow and large, it is usually Kp too high — reduce it by 20–30% steps until oscillation stops.
Can I use PID without an encoder (open-loop)?
No — PID is inherently a closed-loop algorithm requiring feedback. Without measurement of the actual output (encoder counts, temperature reading, pressure sensor), there is no error to compute and the I and D terms become meaningless. The minimum viable setup is your actuator (motor/heater) plus one sensor feeding back the actual state.
Why does my temperature overshoot badly even with small Kp?
Thermal systems have significant lag — the heater puts energy in, but the sensor only sees the effect seconds later. During this lag time the integrator accumulates a large value. Reduce Ki significantly (start at 0.01–0.05), increase the time-proportioning window, and ensure your sensor is physically close to the heated element.
Does the Arduino PID library work on ESP32 and STM32?
Yes. PID_v1 uses standard C++ with no AVR-specific dependencies. It compiles cleanly on ESP32, ESP8266, STM32 (via STM32duino), and Raspberry Pi Pico (Arduino core). You may want to increase sample rates on 32-bit boards since they have faster ADCs and more SRAM.
Understanding and implementing an Arduino PID controller opens the door to genuinely precise and professional-grade projects. Start with the temperature controller — it is forgiving of slow tuning — then move to motor control once you are comfortable reading the response curves. With the right gains and good sensor placement, your system will hit its setpoint smoothly and hold it rock-steady.
Ready to build your PID project? Browse our full range of Arduino boards and modules at Zbotic — all shipped fast across India with GST invoice.
Add comment