Open-loop motor control — simply sending a PWM signal and hoping for the best — is fine for fans and simple actuators. But any serious robotic or automation project requires Arduino encoder motor control with closed-loop feedback. A motor encoder tells your Arduino exactly how far and how fast the shaft has turned, enabling precise speed regulation and position targeting that open-loop control simply cannot achieve. This guide covers everything from reading quadrature encoder pulses to implementing a full PID controller for arduino encoder motor control.
Table of Contents
- Encoder Types and How They Work
- Wiring a Quadrature Encoder to Arduino
- Reading Encoder Pulses with Interrupts
- Calculating Speed from Encoder Data
- PID Control for Motor Speed
- Position Control and Homing
- Using the Encoder Library
- FAQ
Encoder Types and How They Work
Motor encoders come in two main flavours:
Incremental encoders output a series of pulses as the shaft rotates. A basic single-channel encoder gives pulses proportional to speed but cannot determine direction. A quadrature encoder has two channels (A and B) 90° out of phase — the phase relationship between A and B reveals both speed and direction. Most DC gearmotor encoders found in robotics kits are quadrature incremental encoders with resolution ranging from 12 CPR (counts per revolution) to 2000+ CPR depending on the gear ratio and disc resolution.
Absolute encoders output a unique code for every shaft position, so they know absolute position even after power-off. They are more expensive and typically communicate over SPI or I2C. For most Arduino projects, quadrature incremental encoders are the practical choice.
Encoder resolution: Resolution is specified in CPR (counts per revolution of the encoder disc) or PPR (pulses per revolution). With quadrature decoding (counting both edges of both channels — 4X decoding), you get 4× the CPR in actual position counts. A 48 CPR encoder with 4X decoding gives 192 counts/revolution of the encoder shaft, and with a 30:1 gearbox you get 5,760 counts per output shaft revolution — approximately 0.0625° resolution.
Wiring a Quadrature Encoder to Arduino
A typical DC gearmotor with encoder has 6 wires:
- M+ / M−: motor power (connect to motor driver, not Arduino)
- VCC: encoder power supply (3.3V or 5V — check datasheet)
- GND: common ground
- Channel A (ENC_A): first quadrature channel
- Channel B (ENC_B): second quadrature channel
Connect ENC_A and ENC_B to two interrupt-capable pins on the Arduino. On an Uno, use pins 2 and 3. On a Mega, you have pins 2, 3, 18, 19, 20, 21 available. Always connect encoder VCC to the correct voltage rail — many encoders are 3.3V and will be damaged by 5V supply.
// Pin assignment
#define ENC_A 2 // Interrupt pin
#define ENC_B 3 // Interrupt pin
#define MOTOR_PWM 9
#define MOTOR_DIR 8
Also connect the motor driver (e.g., L298N, TB6612) power inputs to an appropriate external supply — never power a motor directly from Arduino’s 5V or Vin pin. Keep encoder signal wires short and away from motor power lines to minimise noise.
Reading Encoder Pulses with Interrupts
The correct way to read a quadrature encoder on Arduino is with hardware interrupts on both encoder channels. Polling-based reading misses pulses at high speed and produces unreliable position data.
Simple 2X decoding (count on rising edge of A only, use B for direction):
volatile long encoderCount = 0;
volatile bool lastA = false;
void encoderISR() {
bool stateA = digitalRead(ENC_A);
bool stateB = digitalRead(ENC_B);
if (stateA != lastA) { // A changed
if (stateA == stateB) {
encoderCount--;
} else {
encoderCount++;
}
lastA = stateA;
}
}
void setup() {
pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_A), encoderISR, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENC_B), encoderISR, CHANGE);
}
By attaching CHANGE interrupts on both channels and comparing their states inside the ISR, this gives full 4X quadrature decoding — maximum resolution from your encoder. The encoderCount increases for forward rotation and decreases for reverse.
Calculating Speed from Encoder Data
Speed is the rate of change of position. Sample encoderCount at a fixed time interval and divide the delta count by the interval and encoder resolution:
const int COUNTS_PER_REV = 1440; // Adjust for your encoder + gearing
const float SAMPLE_TIME_S = 0.05; // 50ms sample period
long prevCount = 0;
float currentRPM = 0;
unsigned long lastSampleTime = 0;
void updateSpeed() {
unsigned long now = millis();
if (now - lastSampleTime >= (unsigned long)(SAMPLE_TIME_S * 1000)) {
noInterrupts();
long currentCount = encoderCount;
interrupts();
long delta = currentCount - prevCount;
prevCount = currentCount;
// RPM = (counts/sample) / (counts/rev) / (seconds/sample) * 60
currentRPM = ((float)delta / COUNTS_PER_REV) / SAMPLE_TIME_S * 60.0;
lastSampleTime = now;
}
}
Always use noInterrupts() / interrupts() when reading the 32-bit encoderCount in main code on an 8-bit AVR to prevent corrupted reads mid-ISR-update.
PID Control for Motor Speed
With speed measurement in place, a PID controller closes the loop. The controller computes an error (setpoint RPM − measured RPM) and adjusts the motor PWM output to drive the error toward zero.
PID output = Kp × error + Ki × ∫error dt + Kd × d(error)/dt
float setpointRPM = 100.0;
float Kp = 2.0, Ki = 5.0, Kd = 0.1;
float integral = 0, prevError = 0;
int computePID(float measured) {
float error = setpointRPM - measured;
integral += error * SAMPLE_TIME_S;
integral = constrain(integral, -50, 50); // Anti-windup
float derivative = (error - prevError) / SAMPLE_TIME_S;
prevError = error;
float output = Kp * error + Ki * integral + Kd * derivative;
return constrain((int)output, 0, 255);
}
void loop() {
updateSpeed();
if (millis() - lastSampleTime == 0) { // Fresh sample
int pwm = computePID(currentRPM);
analogWrite(MOTOR_PWM, pwm);
}
}
Tuning PID gains: Start with Ki = Kd = 0. Increase Kp until the motor responds quickly but oscillates slightly. Then add Ki slowly to eliminate steady-state error. Add a small Kd to damp oscillations. The integral anti-windup clamp (constrain) is essential — without it, the integral term accumulates during stall and causes violent overshoot on recovery.
For a cleaner implementation, use the Arduino PID Library by Brett Beauregard (available in Library Manager) which handles the timing, output limits, and directional logic robustly.
Position Control and Homing
Position control targets a specific encoder count rather than a velocity setpoint. The approach is similar — compute error between target count and current count, run through a P or PD controller:
long targetCount = 0; // Target position in encoder counts
float Kp_pos = 1.5;
float Kd_pos = 0.05;
long prevPosError = 0;
void positionControl() {
noInterrupts();
long pos = encoderCount;
interrupts();
long error = targetCount - pos;
long dError = error - prevPosError;
prevPosError = error;
float output = Kp_pos * error + Kd_pos * dError;
output = constrain(output, -255, 255);
if (output > 0) {
digitalWrite(MOTOR_DIR, HIGH);
analogWrite(MOTOR_PWM, (int)output);
} else {
digitalWrite(MOTOR_DIR, LOW);
analogWrite(MOTOR_PWM, (int)(-output));
}
}
Homing routine: Drive the motor slowly in one direction until a limit switch triggers. Reset encoderCount = 0 at the limit. Now all positions are relative to the known home position — exactly how CNC machines and 3D printers home their axes.
Using the Encoder Library
Paul Stoffregen’s Encoder library handles the ISR setup, quadrature decoding, and thread-safe count access for you. It automatically uses interrupt pins where available and falls back to pin-change interrupts or polling for non-interrupt pins.
#include <Encoder.h>
Encoder myEnc(2, 3); // Channel A on pin 2, B on pin 3
void setup() {
Serial.begin(115200);
}
void loop() {
long position = myEnc.read();
Serial.println(position);
delay(100);
}
Install via Library Manager: search for “Encoder” by Paul Stoffregen. This library is highly optimised and handles all the edge cases discussed in this article, including 4X decoding and atomic reads on AVR. It is the recommended approach for new projects where you do not need full control over the ISR implementation.
FAQ
What encoder resolution do I need for a robotics project?
For wheeled robots, 500–2000 effective counts per output shaft revolution gives good odometry accuracy. With a 48 CPR encoder and 30:1 gearbox with 4X decoding, you get 5,760 counts/rev — about 0.06° resolution, sufficient for most navigation tasks. Higher resolution is better for position control but generates more ISR load at high speeds.
Can I use a single encoder channel (no quadrature) on Arduino?
Yes, but you lose direction information. A single-channel encoder can only measure speed magnitude — you need external direction logic (from the motor driver H-bridge direction pin) to determine motor direction. For basic speed monitoring this is acceptable, but for position control quadrature is essential.
Why does my encoder count jump unexpectedly?
Common causes: (1) Electrical noise — keep encoder wires short, add 100 nF capacitors between each encoder output and ground near the Arduino pin. (2) Missing interrupts — if the motor spins faster than the ISR can handle, use a lower-resolution encoder or a faster MCU. (3) Bouncing mechanical encoder — less common on optical encoders, common on cheap magnetic encoders. (4) Non-atomic reads — always use noInterrupts() around 32-bit reads on AVR.
How do I control two motors independently with one Arduino?
Use Arduino Mega which has 6 interrupt pins — allocate 2 per encoder (4 total for 2 motors), leaving 2 spare. Run two independent PID loops in your code, one per motor, sharing the same sample period. Alternatively, use a dedicated motor controller IC (e.g., RMCS-220x) that handles encoder feedback internally and accepts speed commands over UART or I2C, freeing the Arduino from ISR load entirely.
What is the difference between speed control and position control?
Speed control maintains a target RPM regardless of load. Position control moves the shaft to a target angle and holds it there. Speed control typically uses a PI controller (no derivative needed). Position control benefits from a PD controller (derivative damps overshoot at target). Some applications combine both: an outer position loop generates a speed setpoint for an inner speed loop — called cascaded or nested PID control.
Build your next precision motor project with the right Arduino hardware. Browse Arduino boards and shields at Zbotic — everything you need for encoder motor control projects, shipped across India.
Add comment