KY-040 Encoder and a TM1637 Display: an Interrupt-Driven Counter
A rotary encoder is an "endless" knob for menus and settings. We decode the quadrature signal, catch every step with an interrupt so none is missed, and show the value on a four-digit TM1637 display.
About the project
A potentiometer has end stops and an analog output. An encoder turns endlessly and gives digital pulses: every click is one step. It also has a push button under the knob, which makes it ideal for menus, volume or thermostat set points. We count the steps with an interrupt and show the number on a TM1637 display.
What you need
- ESP32 DevKit
- KY-040 encoder module
- 4-digit 7-segment TM1637 display
- The TM1637Display library (avishorp)
Wiring
| Module | Pin | ESP32 |
|---|---|---|
| KY-040 | CLK | GPIO25 |
| DT | GPIO26 | |
| SW | GPIO27 | |
| + / GND | 3V3 / GND | |
| TM1637 | CLK | GPIO22 |
| DIO | GPIO23 | |
| VCC / GND | 3V3 / GND |
The KY-040 module already has 10 kΩ pull-ups on CLK and DT, so INPUT is enough. The SW button has no pull-up on the module, so it gets INPUT_PULLUP.
The quadrature signal
Inside the encoder there are two contacts offset by a quarter step: channels A (CLK) and B (DT):
clockwise: CLK ‾‾|__|‾‾ DT ‾|__|‾‾‾
counter-clockwise: CLK ‾|__|‾‾‾ DT ‾‾|__|‾‾
At a CLK edge, look at DT: if the levels are different it is a step one way, if they are equal, the other way. Hence digitalRead(DT) != digitalRead(CLK) in the code.
Why an interrupt
If you poll the encoder in loop(), and loop() has a delay() or slow display output, fast turns get lost. An interrupt calls onClk() at the moment of the edge, whatever the program is doing.
Rules for the handler:
- mark it
IRAM_ATTRso the code sits in fast memory; - make the shared variable
volatile, or the compiler may cache it inloop(); - no
Serial.print()and nodelay()inside: do the minimum.
Code
// KY-040 encoder (interrupt) + TM1637 display
#include <TM1637Display.h>
TM1637Display display(22, 23); // CLK, DIO
const int ENC_CLK = 25, ENC_DT = 26, ENC_SW = 27;
volatile int value = 0;
void IRAM_ATTR onClk() {
// on every rising CLK edge, DT tells the direction
if (digitalRead(ENC_DT) != digitalRead(ENC_CLK)) value--;
else value++;
}
void setup() {
pinMode(ENC_CLK, INPUT);
pinMode(ENC_DT, INPUT);
pinMode(ENC_SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_CLK), onClk, RISING);
display.setBrightness(5);
}
void loop() {
if (digitalRead(ENC_SW) == LOW) value = 0; // pressing the knob resets
display.showNumberDec(value);
delay(20);
}
Good to know
- Bounce. Cheap encoders bounce, and the counter may jump by 2 instead of 1. A 10–100 nF capacitor from CLK and DT to ground helps, or a state-machine library (ESP32Encoder uses the hardware PCNT counter).
- Steps vs clicks. Most KY-040s give one full quadrature cycle per click. Some give two pulses per click: then divide by 2.
- TM1637 is not I2C, even though it also uses two wires. The protocol is similar but has no addresses, so each display needs its own pair of pins.