ESP32 UNO - Rotary Encoder

How do you read a spinning knob with the ESP32 Uno? Connect a rotary encoder to the ESP32 Uno form board, and a few lines of Arduino IDE code give you the turn direction, a click counter and the knob button press. You can read the encoder by polling or with an interrupt.

By the end, you can:

  1. Explain how an incremental rotary encoder makes its signals
  2. Tell a rotary encoder from a potentiometer
  3. Wire the CLK, DT and SW pins to the ESP32 Uno
  4. Read the encoder in a polling loop
  5. Read the encoder with an interrupt and IRAM_ATTR
ESP32 Uno rotary encoder

Components You Need

1×ESP32 Uno-form Board
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×Rotary Encoder
1×Jumper Wires

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

Overview of the Rotary Encoder

By the end of this part, you know what the encoder sends and how to read it.

A rotary encoder turns the movement of a knob into electrical pulses. Your code reads the pulses to learn how far and which way the knob moved. There are two types:

Type What It Gives Best For
Incremental A pulse for each small step Counting knob clicks, menus, volume
Absolute A unique code for each angle Exact position, even after power loss

This tutorial uses the incremental type. It is the cheap module most makers buy.

Rotary Encoder Module Pinout

Each pin has one job.

rotary encoder pinout
Pin Role
CLK (Output A) Gives a full pulse (LOW, HIGH, LOW) for every click in either direction
DT (Output B) Same pulse as CLK but 90 degrees later, so the code can find the direction
SW Push button inside the knob. It is open by default and reads LOW when pressed (with a pull-up)
VCC (+) Power, 3.3V to 5V. Use 3.3V with the ESP32 Uno
GND Ground (0V)

Good to know: the ESP32 Uno pins are 3.3V only. Power the module from 3.3V so its pulled-up signal pins never send 5V into the board.

Rotary Encoder vs Potentiometer

The two parts look alike, but they behave in different ways. See the potentiometer page for the other one.

Feature Rotary Encoder Potentiometer
Turn range Endless About three quarters of a turn
Output Digital pulses Analog voltage
Tells you How much the knob moved Where the knob is

How the Rotary Encoder Works

Inside, a slotted disc is joined to a common ground pin C. Two contact pins, A and B, touch the disc as it turns.

rotary encoder output
  • A and B meet the common pin C in a set order, and the order depends on the turn direction
  • One pin touches ground a little before the other, so the two signals are 90 degrees apart. Engineers name this scheme quadrature encoding
  • Clockwise: A touches ground before B
  • Counter-clockwise: B touches ground before A
How rotary encoder works

The rule your code uses, at the moment A rises from LOW to HIGH:

  1. B reads LOW: clockwise turn
  2. B reads HIGH: counter-clockwise turn

※ NOTE THAT:

A and B are the CLK and DT pins, but the order can differ between makers. The sketches here were tested with the DIYables rotary encoder.

The Program Logic

Only the rising edge of CLK matters.

  1. Watch the CLK pin
  2. When CLK goes from LOW to HIGH, read DT
  3. DT is HIGH: counter-clockwise, subtract 1
  4. DT is LOW: clockwise, add 1

ESP32 Uno Pinout

Each Uno header pin maps to one GPIO number, and your sketch uses the GPIO number. Match D2, D3 and D4 with their GPIOs in this diagram before you connect wires.

ESP32 Uno pinout diagram

How to Connect

Three signal wires go to D2, D3 and D4. Power comes from the 3.3V pin.

The wiring diagram between ESP32 Uno rotary encoder

This image is created using Fritzing. Click to enlarge image

Component Pin ESP32 Uno Pin
CLK D2 (GPIO26)
DT D3 (GPIO25)
SW D4 (GPIO17)
VCC (+) 3.3V
GND GND

Good to know: all three pins are normal I/O pins with no boot restrictions, so they are safe for this project.

WARNING

If you power the module from 5V, its pull-up resistors lift CLK, DT and SW to 5V. ESP32 Uno pins are NOT 5V tolerant, so always use the 3.3V pin.

Sketch 1: Polling the Encoder

This sketch polls the CLK pin inside loop(). A click to the right adds 1, a click to the left subtracts 1, and a knob press prints a message.

/* * This ESP32 Uno code was developed by newbiely.com * * This ESP32 Uno code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-uno/esp32-uno-rotary-encoder */ #include <ezButton.h> // library that handles the SW button and its debounce #define CLK_PIN 26 // D2 (GPIO26) reads the CLK (output A) line of the encoder #define DT_PIN 25 // D3 (GPIO25) reads the DT (output B) line of the encoder #define SW_PIN 17 // D4 (GPIO17) reads the SW push button of the encoder #define DIRECTION_CW 0 // value used for clockwise turns #define DIRECTION_CCW 1 // value used for counter-clockwise turns int counter = 0; int direction = DIRECTION_CW; int CLK_state; int prev_CLK_state; ezButton button(SW_PIN); // button object tied to the SW pin (GPIO17) void setup() { Serial.begin(9600); // both encoder lines are plain digital inputs pinMode(CLK_PIN, INPUT); pinMode(DT_PIN, INPUT); button.setDebounceTime(50); // ignore button bounce shorter than 50 ms // remember where the CLK line starts prev_CLK_state = digitalRead(CLK_PIN); } void loop() { button.loop(); // always update the button first // sample the CLK line now CLK_state = digitalRead(CLK_PIN); // a change on CLK means the knob moved // only the LOW to HIGH edge counts, so one click is counted once if (CLK_state != prev_CLK_state && CLK_state == HIGH) { // DT is HIGH when CLK rises: the knob turns counter-clockwise, so count down if (digitalRead(DT_PIN) == HIGH) { counter--; direction = DIRECTION_CCW; } else { // DT is LOW when CLK rises: the knob turns clockwise, so count up counter++; direction = DIRECTION_CW; } Serial.print("DIRECTION: "); if (direction == DIRECTION_CW) Serial.print("Clockwise"); else Serial.print("Counter-clockwise"); Serial.print(" | COUNTER: "); Serial.println(counter); } // keep the CLK state for the next pass prev_CLK_state = CLK_state; if (button.isPressed()) { Serial.println("The button is pressed"); } }
Code Part What It Does
CLK_PIN 26, DT_PIN 25, SW_PIN 17 GPIO numbers of D2, D3 and D4
ezButton button Reads the SW pin and removes bounce
CLK_state != prev_CLK_state Spots a change on the CLK line
digitalRead(DT_PIN) Decides the direction when CLK rises
counter++ / counter-- Counts clicks up or down

The Serial Monitor prints the direction and the counter after every click. The ezButton library keeps the button code short.

Upload and Test

  1. First time with this board? Set it up with ESP32 Uno - Getting Started before you continue.
  2. Build the circuit by following the wiring table above.
  3. Plug in the USB Type-C cable between the ESP32 Uno and your computer.
  4. Select the board in Arduino IDE: pick ESP32 Dev Module and the COM port of the board (CH340).
  5. Install the ezButton library with this guide: ezButton library.
  6. Copy the code above into Arduino IDE.
  7. Send it to the board with the Upload button.
  8. Open the Serial Monitor at 9600 baud.
  9. Turn the knob left, then right, then press it.
  10. Pro tip: turn the knob slowly at first. If a clockwise turn prints "Counter-clockwise", swap the CLK and DT wires.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32 Dev Module' on 'COM15')
New Line
9600 baud
DIRECTION: Counter-clockwise | COUNTER: -1 DIRECTION: Counter-clockwise | COUNTER: -2 DIRECTION: Counter-clockwise | COUNTER: -3 DIRECTION: Clockwise | COUNTER: -2 DIRECTION: Clockwise | COUNTER: -1 DIRECTION: Clockwise | COUNTER: 0 DIRECTION: Clockwise | COUNTER: 1 The button is pressed
Ln 11, Col 1
ESP32 Dev Module on COM15
2

Video Tutorial

See the ESP32 Uno build in action in the video below.

Sketch 2: Reading the Encoder by Interrupt

Polling wastes time on the ESP32 Uno and can lose clicks when other code in loop() runs for long. An interrupt calls a small function the moment CLK rises, so loop() stays free for other work.

/* * This ESP32 Uno code was developed by newbiely.com * * This ESP32 Uno code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-uno/esp32-uno-rotary-encoder */ #include <ezButton.h> // library that handles the SW button and its debounce #define CLK_PIN 26 // D2 (GPIO26) reads the CLK (output A) line of the encoder #define DT_PIN 25 // D3 (GPIO25) reads the DT (output B) line of the encoder #define SW_PIN 17 // D4 (GPIO17) reads the SW push button of the encoder #define DIRECTION_CW 0 // value used for clockwise turns #define DIRECTION_CCW 1 // value used for counter-clockwise turns volatile int counter = 0; volatile int direction = DIRECTION_CW; volatile unsigned long last_time; // time of the last accepted pulse, used for debounce int prev_counter; ezButton button(SW_PIN); // button object tied to the SW pin (GPIO17) void setup() { Serial.begin(9600); // both encoder lines are plain digital inputs pinMode(CLK_PIN, INPUT); pinMode(DT_PIN, INPUT); button.setDebounceTime(50); // ignore button bounce shorter than 50 ms // one interrupt on CLK is enough // ISR_encoderChange() runs each time CLK goes from LOW to HIGH attachInterrupt(digitalPinToInterrupt(CLK_PIN), ISR_encoderChange, RISING); } void loop() { button.loop(); // always update the button first // print only when the interrupt changed the counter if (prev_counter != counter) { Serial.print("DIRECTION: "); if (direction == DIRECTION_CW) Serial.print("Clockwise"); else Serial.print("Counter-clockwise"); Serial.print(" | COUNTER: "); Serial.println(counter); prev_counter = counter; } if (button.isPressed()) { Serial.println("The button is pressed"); } // you can add other work here } // IRAM_ATTR keeps this function in fast internal RAM on the ESP32 Uno void IRAM_ATTR ISR_encoderChange() { if ((millis() - last_time) < 50) // skip pulses closer than 50 ms (bounce) return; if (digitalRead(DT_PIN) == HIGH) { // DT is HIGH when CLK rises: counter-clockwise, so count down counter--; direction = DIRECTION_CCW; } else { // DT is LOW when CLK rises: clockwise, so count up counter++; direction = DIRECTION_CW; } last_time = millis(); }
  1. Copy the code above into Arduino IDE.
  2. Press Upload again.
  3. Turn and press the knob. The output looks like the first sketch.
Code Part What It Does
attachInterrupt(..., RISING) Runs ISR_encoderChange() on each LOW to HIGH edge of CLK
IRAM_ATTR Keeps the function in fast internal RAM, which the ESP32 Uno needs for interrupt functions
volatile variables Tell the compiler that the interrupt can change them
millis() - last_time Drops pulses closer than 50 ms (bounce)
prev_counter != counter Prints only when the value changed

※ NOTE THAT:

  • Most GPIO pins on the ESP32 Uno can trigger an interrupt, so you are not limited to D2 and D3
  • A single interrupt on the CLK pin does the job. A second one is not needed
  • Any global variable changed inside the interrupt must be volatile
  • Keep the interrupt routine tiny. Never call Serial.print() inside it

ESP32 Uno vs UNO R4 for This Project

Wiring stays the same on the header, but the sketch needs GPIO numbers and one new keyword.

Item Arduino UNO R4 ESP32 Uno
Logic voltage 5V 3.3V
CLK pin 2 D2 (GPIO26)
DT pin 3 D3 (GPIO25)
SW pin 4 D4 (GPIO17)
Interrupt function Plain function Needs IRAM_ATTR
Interrupt pins Only pins 2 and 3 Most GPIO pins
Arduino IDE board Arduino UNO R4 WiFi ESP32 Dev Module

The same header positions work as they are. Power the encoder from 3.3V instead of 5V.

Go Wireless: Show the Count on Your Phone

Send the counter over WiFi or Bluetooth, and you can watch the knob without a USB cable.

The ESP32 Uno has WiFi and Bluetooth, so the click counter can appear on a web page or in a phone app. Instead of Serial.println(counter), push the value to a monitor or a plotter. A plotter draws the rise and fall of the counter as you turn the knob.

Try These Next

  • Reset the counter to 0 when the knob is pressed
  • Limit the counter between 0 and 100 for a volume control
  • Use two encoders on two sets of pins
  • Move a servo or change LED brightness with the counter

Function References

Troubleshooting

Problem Possible Cause Solution
No COM port in Arduino IDE CH340 driver missing or charge-only cable Install the CH340 driver and use a data USB cable
Upload fails with Failed to connect or Waiting for download Board not in download mode Hold the BOOT button while the upload starts
Serial Monitor stays empty while you turn A wire is loose or the module has no power Re-check 3.3V, GND, and the CLK (GPIO26) and DT (GPIO25) wires
Direction is reversed CLK and DT swapped on your module Swap the two wires or swap CLK_PIN and DT_PIN in the code
Counter jumps by more than 1 per click Contact bounce or a missed edge Use the interrupt sketch with its 50 ms filter
Knob press is not seen SW wire loose or wrong pin Check SW goes to D4 (GPIO17) and the code uses 17
Compile error ezButton.h not found Library missing Install ezButton in the Library Manager
Interrupt sketch crashes the board Interrupt function has no IRAM_ATTR Add IRAM_ATTR before the function name
Garbled text in Serial Monitor Wrong baud rate Set the Serial Monitor to 9600 baud

FAQ

Can I power the rotary encoder from 5V with the ESP32 Uno?

No. The module pulls CLK, DT and SW up to VCC, so 5V power puts 5V on the board pins. The logic level of this board is 3.3V. The 3.3V pin works well for this module.

What is the difference between polling and an interrupt for an encoder?

Polling reads the pin again and again in loop(). An interrupt runs a small function as soon as the pin changes. Interrupts miss fewer clicks when loop() has other slow work.

Why does the interrupt function need IRAM_ATTR on the ESP32 Uno?

The ESP32 Uno runs code from flash, which can be busy. IRAM_ATTR puts the function in internal RAM, so it runs at once when the interrupt fires.

Can I use any pin for the encoder interrupt?

Mostly yes. On the ESP32 Uno most GPIO pins can trigger interrupts, unlike the UNO R4, which allows only pins 2 and 3. Avoid D0 and D1 because they carry the USB serial, and avoid the boot strapping pins for this module.

Why does the counter skip or jump by two?

Contact bounce is the usual cause. The interrupt sketch ignores pulses closer than 50 ms. Keep loop() short if you use polling.

How do I reset the counter with the knob button?

Set counter = 0; inside the if (button.isPressed()) block. In the interrupt sketch, set prev_counter = 0; too, or the reset value will not show.

Can I use the UNO R4 sketch with pins 2, 3 and 4 without changes?

No. The sketch must use GPIO numbers 26, 25 and 17 instead of 2, 3 and 4. The wiring on the header stays the same.

Can I use pins A2 to A5 for the encoder?

They are input only with no internal pull-up, so an encoder needs external 10k pull-up resistors to 3.3V there. The D pins are simpler for this project.

Can I show the encoder count on a web page with the ESP32 Uno?

Yes. The ESP32 Uno has WiFi and Bluetooth, so you can send the counter to a web monitor or a phone app. See the Go Wireless section for links.

What if I run out of header pins for my project?

The ESP32 Uno has a few extra holes near the power header: GPIO32 and GPIO33 (I/O, PWM, analog on ADC1, so they also work while WiFi is on) and GPIO15 (I/O, PWM, analog on ADC2). Add pin headers by soldering, then use the plain GPIO number in your sketch. For a second encoder, GPIO32 and GPIO33 are good first choices. Two warnings: (1) GPIO15 and GPIO0 (the IO0 pin at the unused Uno NC position) are boot strapping pins, so a wrong connection can stop the board from booting or uploading code. Use GPIO32 and GPIO33 first. (2) Never use the six holes under the digital header (SD0 to SD3, CMD and CLK, which are GPIO6 to GPIO11). They are wired to the flash chip, and using them crashes the board.

Learn More

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!