Rotary Encoder LCD Menu
ESP32 DevKit (WROOM-32)·Arduino C++·Updated Jul 27, 2026·by FluxBench TeamIncludes Lab + Schematic
A knob-driven menu system on a 16x2 character LCD - the classic control panel combo. The ESP32Encoder library counts KY-040 quadrature pulses with the ESP32 hardware pulse counter (CLK GPIO 26, DT GPIO 27, half-quad = 2 counts per detent), so every detent scrolls the menu exactly one item, in either direction. Push the shaft (GPIO 14) to run the highlighted action: switch the LED on GPIO 4 on or off, blink it three times, show the live encoder count, or an about screen. In the Lab, hover the encoder knob and scroll to rotate it, click the knob to press it, and watch the LCD panel update.
sketch.ino
// Rotary Encoder LCD Menu - turn the knob to scroll a menu on a classic
// 16x2 character LCD, push the shaft to run the highlighted action.
// The ESP32Encoder library counts KY-040 quadrature pulses with the
// ESP32's hardware pulse counter (PCNT), so no polling races:
// encoder.getCount() always has the truth, 2 counts per detent here.
// In the Lab, hover the encoder knob and scroll to rotate it; click to press.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Encoder.h>
#define CLK_PIN 26
#define DT_PIN 27
#define SW_PIN 14 // encoder push switch, active LOW
#define LED_PIN 4
#define ITEMS 5
LiquidCrystal_I2C lcd(0x27, 16, 2);
ESP32Encoder encoder;
int sel = 0;
long lastCount = 0;
bool inItem = false;
bool swWas = true;
unsigned long lastPress = 0;
const char* itemName = "LED ON";
void updateName(int n) {
itemName = "ABOUT";
if (n == 0) itemName = "LED ON";
else if (n == 1) itemName = "LED OFF";
else if (n == 2) itemName = "BLINK 3X";
else if (n == 3) itemName = "COUNT";
}
void drawMenu() {
updateName(sel);
lcd.clear();
lcd.print(">");
lcd.print(itemName);
lcd.setCursor(13, 0);
lcd.print(sel + 1);
lcd.print("/5");
lcd.setCursor(0, 1);
lcd.print("turn+push knob");
}
void runItem() {
lcd.clear();
lcd.setCursor(0, 0);
if (sel == 0) {
digitalWrite(LED_PIN, HIGH);
lcd.print("LED is ON");
Serial.println("ACTION: LED ON");
} else if (sel == 1) {
digitalWrite(LED_PIN, LOW);
lcd.print("LED is OFF");
Serial.println("ACTION: LED OFF");
} else if (sel == 2) {
lcd.print("Blinking...");
Serial.println("ACTION: BLINK 3X");
for (int k = 0; k < 3; k++) {
digitalWrite(LED_PIN, HIGH);
delay(120);
digitalWrite(LED_PIN, LOW);
delay(120);
}
} else if (sel == 3) {
lcd.print("Count ");
lcd.print(encoder.getCount());
Serial.print("ACTION: COUNT ");
Serial.println(encoder.getCount());
} else {
lcd.print("FluxBench demo");
Serial.println("ACTION: ABOUT");
}
lcd.setCursor(0, 1);
lcd.print("push = back");
}
void setup() {
Serial.begin(115200);
pinMode(SW_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
ESP32Encoder::useInternalWeakPullResistors = puType::up;
encoder.attachHalfQuad(DT_PIN, CLK_PIN); // 2 counts per detent
encoder.setCount(0);
lcd.init();
lcd.backlight();
Serial.println("Rotary menu ready - turn to scroll, push to select");
drawMenu();
}
void loop() {
long c = encoder.getCount();
long diff = c - lastCount;
if (!inItem && diff >= 2) { // one detent clockwise
sel = (sel + 1) % ITEMS;
lastCount = lastCount + 2;
updateName(sel);
Serial.print("MENU: ");
Serial.println(itemName);
drawMenu();
} else if (!inItem && diff <= -2) { // one detent counter-clockwise
sel = (sel + ITEMS - 1) % ITEMS;
lastCount = lastCount - 2;
updateName(sel);
Serial.print("MENU: ");
Serial.println(itemName);
drawMenu();
} else if (inItem) {
lastCount = c; // ignore turns inside an item screen
}
bool sw = digitalRead(SW_PIN);
if (swWas && !sw && millis() - lastPress > 200) {
lastPress = millis();
if (inItem) {
inItem = false;
Serial.println("BACK to menu");
drawMenu();
} else {
inItem = true;
runItem();
}
}
swWas = sw;
delay(5);
}Built with FluxBench.AI — the AI workbench for embedded projects.