← All projects
Displays Intermediate

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.

Difficulty
Intermediate
Time
40 min
Category
Displays
Updated

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_ATTR so the code sits in fast memory;
  • make the shared variable volatile, or the compiler may cache it in loop();
  • no Serial.print() and no delay() 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.