Componentes · 16 min de lectura

Encoder rotativo Arduino: KY-040, interrupciones y menú LCD

Encoder rotativo con Arduino: conexión del KY-040, señal A/B en cuadratura, interrupciones sin perder pasos, antirrebote, menú LCD, ESP32 PCNT y Pico.

Un encoder rotativo es una perilla de control digital para proyectos con Arduino que gira sin fin, cuenta un paso por cada clic y funciona como pulsador cuando presionas su eje. Conectado a un Arduino, el encoder rotativo se comporta como la perilla del menú de una impresora 3D, el mando de ajuste de un osciloscopio o el control de volumen de un coche: sabe cuántos clics has girado y en qué sentido, y nunca llega a un tope. El modelo que más vas a encontrar en los kits de electrónica es el módulo KY-040 de cinco pines.

En esta guía aprenderás en qué se diferencia el KY-040 de un potenciómetro, cómo se obtiene el sentido de giro a partir de las fases A y B (cuadratura), cómo filtrar el rebote de los contactos por hardware y por software, y cómo contar cada clic sin perder ninguno usando interrupciones. Después montaremos paso a paso el atajo con la librería Encoder, la navegación por menús en LCD y OLED, el contador por hardware PCNT del ESP32 y el código MicroPython para la Raspberry Pi Pico.

¿Qué es un encoder rotativo y cómo funciona?

Dentro de un encoder mecánico como el KY-040 hay un disco de contactos dentado que gira con el eje y dos pequeños interruptores que se deslizan sobre él. Al girar el eje, los dos interruptores se abren y se cierran uno tras otro y en los pines CLK (A) y DT (B) aparecen pulsos de onda cuadrada. Las dos ondas están desfasadas un cuarto de periodo, es decir, 90 grados; a esto se le llama señal en cuadratura. El microcontrolador deduce el sentido de giro según qué onda cambia primero, y cuánto ha girado contando los cambios. El encoder no recuerda su posición, solo informa del movimiento. Por eso el KY-040 es un encoder incremental: al encender la posición es cero y todo el conteo lo hace tu código.

CaracterísticaValor del KY-040
TipoMecánico, incremental, salidas A/B
Clics (retenes)20 clics por vuelta (30 en algunos encoders)
Pulsos20 ciclos A/B completos por vuelta, es decir, 80 flancos
Resolución angular18 grados por clic
PinesCLK (A), DT (B), SW (pulsador), + (alimentación), GND
Alimentación3,3–5 V (solo alimenta las resistencias pull-up)
Resistencias integradasPull-up de 10 kΩ en CLK y DT; la resistencia de SW no viene montada en todos los módulos
PulsadorNormalmente abierto; al presionar el eje, SW se conecta a GND (activo en LOW)
GiroIlimitado, sin tope

Encoder rotativo vs potenciómetro: ¿cuál es la diferencia?

A primera vista un encoder se parece mucho a un potenciómetro: los dos tienen un eje giratorio y los dos sirven para ajustar un valor. Pero funcionan de forma completamente distinta. Un potenciómetro es un divisor de tensión; según la posición del eje entrega una tensión analógica entre 0 y 5 V, que lees con analogRead. Un encoder, en cambio, genera pulsos en lugar de una tensión; no te dice la posición, sino el cambio.

Potenciómetro para comparar con un encoder rotativo
Potenciómetro: su giro está limitado a unos 270–300 grados y entrega la posición como una tensión analógica. Un encoder rotativo gira sin parar y produce pulsos digitales.
CaracterísticaPotenciómetroEncoder rotativo (KY-040)
GiroUnos 270–300 grados, limitadoIlimitado, 360 grados continuos
SalidaTensión analógica (0–5 V)Pulsos digitales (A y B)
LecturaanalogRead (ADC)digitalRead o conteo con interrupciones
PosiciónAbsoluta; la posición del eje da el valor incluso tras un corte de luzRelativa; la posición es desconocida al encender
Sensación de clicNo, gira suaveSí, normalmente 20 clics por vuelta
PulsadorNoSí (presionando el eje)
Problema típicoRuido y valores que bailan con el tiempoRebote de contactos (se resuelve por software)
Uso típicoVolumen, ángulo de servo, ajuste analógicoNavegación por menús, ajustes numéricos por pasos, paneles de equipos

En resumen, si el valor debe corresponder exactamente a la posición física del eje (mover a mano el brazo de un servo o un simple control de volumen), el potenciómetro es más sencillo. Si quieres moverte por un menú, subir y bajar un número en pasos precisos, cambiar varios ajustes con una sola perilla o modificar el valor también desde el software, el encoder es mucho más flexible. Por ejemplo, si un valor que marca 50 se cambia a 80 desde el móvil, al girar el encoder simplemente sigues desde 80; con un potenciómetro, la posición de la perilla y el valor de la pantalla dejan de coincidir.

Fases A y B: cómo funciona la lectura en cuadratura

Cuando el KY-040 está en reposo en un clic, los dos interruptores están abiertos y, gracias a las resistencias pull-up, CLK y DT leen HIGH (1). Al girar la perilla un clic, las dos líneas bajan a LOW una tras otra y vuelven a HIGH una tras otra. En sentido horario baja primero CLK; en sentido antihorario, primero DT. Esta secuencia de cuatro estados es un código Gray: en cada paso cambia un solo bit. La tabla muestra lo que leen los dos pines durante un clic de movimiento:

PasoSentido horario (CLK, DT)Sentido antihorario (CLK, DT)
En reposo en un clic1, 11, 1
1.ª transición0, 11, 0
2.ª transición0, 00, 0
3.ª transición1, 00, 1
4.ª transición (siguiente clic)1, 11, 1

De la tabla salen dos reglas sencillas. Primera: si DT sigue en HIGH cuando CLK baja, el giro es horario; si DT ya está en LOW, es antihorario. Segunda: un clic tiene cuatro transiciones, así que 20 clics por vuelta son 80 transiciones. El código sencillo solo usa la primera regla y cuenta una vez por clic. El código robusto sigue las cuatro transiciones y así puede descartar movimientos con rebote o a medias. Qué sentido se considera horario puede variar según el módulo y el cableado; si tu contador avanza al revés, basta con intercambiar los pines CLK y DT en el código.

Pines del KY-040 y conexión

Pin del KY-040FunciónArduino UnoESP32Raspberry Pi Pico
CLKFase AD2GPIO 25GP16
DTFase BD3GPIO 26GP17
SWPulsador (LOW al pulsar)D4GPIO 27GP18
+Alimentación5V3V33V3(OUT)
GNDTierraGNDGNDGND
En el ESP32 y la Raspberry Pi Pico conecta siempre el pin + del módulo a 3,3 V. Las resistencias pull-up del KY-040 van al pin +, así que si alimentas el módulo con 5 V, CLK y DT quedan a 5 V y se supera el límite de las entradas de 3,3 V. Como el encoder solo tiene interruptores, funciona perfectamente a 3,3 V.

En el Arduino Uno y el Nano solo hay interrupciones por hardware en los pines D2 y D3, así que lo mejor es conectar ahí CLK y DT. En el Mega los pines de interrupción son 2, 3, 18, 19, 20 y 21; en el Leonardo, 0, 1, 2, 3 y 7. En el ESP32 y la Pico casi cualquier GPIO puede generar una interrupción. Puedes consultar el mapa de pines de tu placa en la herramienta Pinout. Usa INPUT_PULLUP para el pin SW en el código: algunos módulos no tienen resistencia en esa línea y un pin al aire se lee como si se pulsara al azar. En el ESP32, los GPIO 34–39 son solo de entrada y no tienen pull-up interna; no los uses para SW.

Primera prueba paso a paso: lectura por sondeo (polling)

  1. Conecta el pin CLK del KY-040 a D2, DT a D3 y SW a D4.
  2. Conecta el pin + a 5V y GND al GND del Arduino.
  3. Pega el código siguiente en el Arduino IDE y súbelo a la placa.
  4. Abre el Monitor Serie y ajusta la velocidad a 115200 baudios.
  5. Gira la perilla unos clics en ambos sentidos y luego presiona el eje para poner el contador a cero.
Arduino (C++)
1// KY-040: CLK -> D2, DT -> D3, SW -> D4, + -> 5V, GND -> GND
2const byte PIN_CLK = 2;
3const byte PIN_DT  = 3;
4const byte PIN_SW  = 4;
5
6int counter = 0;
7int lastClk;
8
9void setup() {
10  pinMode(PIN_CLK, INPUT_PULLUP);   // modülde 10k pull-up zaten var / the module already has 10k pull-ups
11  pinMode(PIN_DT, INPUT_PULLUP);
12  pinMode(PIN_SW, INPUT_PULLUP);    // basınca LOW okunur / reads LOW when pressed
13  Serial.begin(115200);
14  lastClk = digitalRead(PIN_CLK);
15}
16
17void loop() {
18  int clk = digitalRead(PIN_CLK);
19  if (clk != lastClk && clk == LOW) {      // CLK düşen kenar / CLK falling edge
20    if (digitalRead(PIN_DT) == HIGH) {     // DT hâlâ HIGH: saat yönü / DT still HIGH: clockwise
21      counter++;
22      Serial.print("CW  ");
23    } else {                               // DT zaten LOW: ters yön / DT already LOW: counter-clockwise
24      counter--;
25      Serial.print("CCW ");
26    }
27    Serial.println(counter);
28  }
29  lastClk = clk;
30
31  if (digitalRead(PIN_SW) == LOW) {        // mile basıldı / shaft pressed
32    delay(20);                             // buton sıçramasını bekle / wait out the button bounce
33    if (digitalRead(PIN_SW) == LOW) {
34      counter = 0;
35      Serial.println("Sifirlandi / reset");
36      while (digitalRead(PIN_SW) == LOW) {}   // bırakılmasını bekle / wait for release
37    }
38  }
39}

En cada vuelta de loop el código lee CLK y solo detecta el momento en que CLK baja de HIGH a LOW. Si en ese instante DT sigue en HIGH, la perilla ha girado en sentido horario; si está en LOW, en sentido antihorario. Es el método más fácil de entender, pero tiene dos puntos débiles. Primero, los pulsos que llegan mientras loop está ocupado con otras tareas (actualizar una pantalla, delay, leer sensores) se pierden y el contador se queda atrás si giras rápido. Segundo, los contactos mecánicos rebotan: CLK puede pasar varias veces de LOW a HIGH al bajar una sola vez, y el código puede contar un clic como dos o tres. Las dos secciones siguientes resuelven ambos problemas.

Rebote de contactos (antirrebote): solución por hardware y software

Los muelles metálicos del interior del encoder rebotan varias veces al tocar el contacto antes de asentarse. En las hojas de datos de los fabricantes este tiempo suele indicarse en unos pocos milisegundos (por ejemplo, 2 ms como máximo). Con los pulsadores resuelves el rebote con "esperar 20–50 ms tras el último cambio", pero en un encoder eso no sirve: cuando se gira rápido solo hay unos milisegundos entre dos flancos, y una espera larga también se come pasos reales. En su lugar se usan estos métodos:

  • Filtro por hardware: Coloca un condensador de 10–100 nF entre CLK y GND y otro entre DT y GND. Junto con las pull-up de 10 kΩ del módulo forman un filtro RC; la constante de tiempo va de 10 kΩ × 10 nF = 0,1 ms a 10 kΩ × 100 nF = 1 ms y absorbe los rebotes cortos. Puedes calcularla con la herramienta constante de tiempo RC y descifrar un condensador marcado 103 o 104 con la herramienta código de condensadores.
  • Tabla de estados: En cada flanco de las dos líneas se comparan el estado AB anterior y el nuevo. Un rebote significa ir y venir entre los mismos dos estados, lo que produce +1 y −1 alternados que se anulan. Las transiciones en las que cambian los dos bits a la vez se consideran inválidas y se ignoran.
  • Decidir solo en el clic: Los cuartos de paso se acumulan y el contador solo sube o baja cuando el encoder se asienta en un clic (las dos líneas en HIGH). Así, un contacto que vibra sobre el clic no hace saltar el cursor del menú.
  • Bloqueo temporal para el pulsador: El pin SW se comporta como un pulsador normal; basta con un bloqueo de 30 ms usando millis().

Lectura con interrupciones para no perder ningún paso

Una interrupción hace que el procesador deje lo que está haciendo en cuanto cambia un pin y ejecute una función corta (ISR). Al conectar las líneas del encoder a interrupciones, no se pierde ningún flanco aunque loop tenga un delay de 50 ms o la pantalla se actualice despacio. El código siguiente se ejecuta en ambos flancos de CLK y DT, usa la tabla de estados descrita arriba y solo cuenta cuando la perilla se asienta en un clic. Para los fundamentos, consulta también el artículo interrupciones en Arduino.

Arduino (C++)
1// KY-040: CLK -> D2 (INT0), DT -> D3 (INT1), SW -> D4, + -> 5V
2const byte PIN_CLK = 2;
3const byte PIN_DT  = 3;
4const byte PIN_SW  = 4;
5
6// İndeks: (eski AB << 2) | yeni AB / Index: (old AB << 2) | new AB
7const int8_t QEM[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
8
9volatile long position = 0;   // tık sayısı, ISR ile loop paylaşır / detent count shared by ISR and loop
10uint8_t lastAB = 3;           // son durum, 3 = ikisi de HIGH / last state, 3 = both HIGH
11int8_t quarter = 0;           // iki tık arası çeyrek adımlar / quarter steps between detents
12
13void readEncoder() {
14  uint8_t ab = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
15  quarter += QEM[(lastAB << 2) | ab];
16  lastAB = ab;
17  if (ab == 3) {                          // tıka oturdu / settled in a detent
18    if (quarter > 1) position++;          // saat yönü / clockwise
19    else if (quarter < -1) position--;    // ters yön / counter-clockwise
20    quarter = 0;
21  }
22}
23
24bool buttonPressed() {
25  static bool lastState = HIGH;
26  static unsigned long lastChange = 0;
27  bool state = digitalRead(PIN_SW);
28  if (state != lastState && millis() - lastChange > 30) {   // 30 ms kilit / 30 ms lockout
29    lastChange = millis();
30    lastState = state;
31    return state == LOW;                  // yalnızca basış anı / only the moment of pressing
32  }
33  return false;
34}
35
36void setup() {
37  pinMode(PIN_CLK, INPUT_PULLUP);
38  pinMode(PIN_DT, INPUT_PULLUP);
39  pinMode(PIN_SW, INPUT_PULLUP);
40  Serial.begin(115200);
41  lastAB = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
42  attachInterrupt(digitalPinToInterrupt(PIN_CLK), readEncoder, CHANGE);   // her iki kenar / both edges
43  attachInterrupt(digitalPinToInterrupt(PIN_DT), readEncoder, CHANGE);
44}
45
46long lastPosition = 0;
47
48void loop() {
49  noInterrupts();                         // 4 baytı güvenle kopyala / copy the 4 bytes safely
50  long pos = position;
51  interrupts();
52
53  if (pos != lastPosition) {
54    Serial.print(pos > lastPosition ? "CW  " : "CCW ");
55    Serial.println(pos);
56    lastPosition = pos;
57  }
58
59  if (buttonPressed()) {
60    noInterrupts();
61    position = 0;
62    interrupts();
63    Serial.println("Sifirlandi / reset");
64  }
65
66  delay(50);   // loop yavaş olsa da tık kaçmaz / no detent is lost even with a slow loop
67}
Parte del código¿Qué hace?
Tabla QEM[16]Convierte el estado AB anterior y el nuevo en +1, −1 o 0; las transiciones inválidas en las que cambian las dos líneas a la vez cuentan como 0
attachInterrupt(..., CHANGE)Ejecuta readEncoder() en el flanco de subida y de bajada de CLK y DT
quarterAcumula los cuartos de paso entre dos clics; los pares +1 y −1 del rebote se anulan
if (ab == 3)Solo se decide cuando el encoder se asienta en un clic (las dos líneas en HIGH); un acumulado de +2 o más cuenta como un clic
volatile long positionLa variable que comparten la ISR y loop; volatile impide que el compilador use una copia desactualizada
noInterrupts() / interrupts()Evita que la ISR se cuele mientras se copia el long de 4 bytes
buttonPressed()Antirrebote de SW con bloqueo de 30 ms; devuelve true una sola vez, en el momento de pulsar
Mantén la ISR lo más corta posible: no uses dentro Serial.print, delay ni comandos de pantalla; actualiza solo el contador y deja el trabajo real para loop. Declara como volatile toda variable que compartan la ISR y loop. Como el Uno tiene un procesador de 8 bits, desactivar brevemente las interrupciones al leer una variable de 2 o 4 bytes evita que leas un valor actualizado a medias.

El atajo: la librería Encoder

Si no quieres escribir tu propia ISR, la librería Encoder de Paul Stoffregen hace lo mismo con código optimizado. Abre el Gestor de librerías del Arduino IDE, busca "Encoder" e instala la librería de Paul Stoffregen; para los pasos de instalación, echa un vistazo a la guía cómo añadir una librería en Arduino. La librería ofrece el mejor rendimiento cuando los dos pines son de interrupción, buen rendimiento cuando solo lo es el primer pin que se pasa al constructor y un rendimiento bajo cuando ninguno lo es. Como cuenta todos los flancos, el valor cambia 4 por clic; divide entre 4 para obtener el número de clics.

Arduino (C++)
1#include <Encoder.h>
2
3// Önce DT (3), sonra CLK (2): saat yönü artı sayılır / DT (3) first, then CLK (2): clockwise counts up
4Encoder knob(3, 2);
5const byte PIN_SW = 4;
6long lastDetent = 0;
7
8void setup() {
9  pinMode(PIN_SW, INPUT_PULLUP);
10  Serial.begin(115200);
11}
12
13void loop() {
14  long detent = knob.read() / 4;          // 4 kenar = 1 tık / 4 edges = 1 detent
15  if (detent != lastDetent) {
16    lastDetent = detent;
17    Serial.println(detent);
18  }
19  if (digitalRead(PIN_SW) == LOW) {       // basınca sıfırla / reset on press
20    knob.write(0);
21  }
22}

read() devuelve el número total de flancos hasta el momento, write(0) pone el contador a cero y readAndReset() lee y pone a cero a la vez. Pasamos primero DT y después CLK al constructor por el signo del sentido de giro: en este orden el sentido horario cuenta en positivo. Si a ti te sale al revés, intercambia 2 y 3. La librería también activa por sí sola las resistencias pull-up internas de los pines.

Pantalla LCD I2C de 16x2
Pantalla LCD I2C de 16x2: se conecta solo con los cables SDA y SCL; los ajustes del ejemplo de menú se muestran en esta pantalla.

Donde más se usan los encoders es en los menús: giras la perilla para moverte entre líneas, la pulsas para poner el ajuste seleccionado en modo edición, giras para cambiar el valor y vuelves a pulsar para regresar al menú. El ejemplo siguiente ajusta los canales rojo, verde y azul de un LED RGB entre 0 y 255 y muestra los valores en una pantalla LCD I2C. Conecta el LCD a A4 (SDA) y A5 (SCL), y las patas de color de un LED RGB de cátodo común a D9, D10 y D11 con resistencias de 220 Ω.

Arduino (C++)
1#include <Wire.h>
2#include <LiquidCrystal_I2C.h>
3
4LiquidCrystal_I2C lcd(0x27, 16, 2);           // adres 0x27 ya da 0x3F / address 0x27 or 0x3F
5
6const byte PIN_CLK = 2, PIN_DT = 3, PIN_SW = 4;
7const byte COUNT = 3;
8const byte LED_PINS[COUNT] = {9, 10, 11};     // RGB LED, PWM pinleri / PWM pins
9const char* NAMES[COUNT] = {"Kirmizi/Red", "Yesil/Green", "Mavi/Blue"};
10int values[COUNT] = {0, 0, 0};                // 0-255 parlaklık / brightness
11
12const int8_t QEM[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
13volatile int steps = 0;                       // loop'un işlemediği tıklar / detents not yet handled by loop
14uint8_t lastAB = 3;
15int8_t quarter = 0;
16
17int selected = 0;                             // seçili satır / selected item
18bool editing = false;                         // değer düzenleme modu / value edit mode
19
20void readEncoder() {
21  uint8_t ab = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
22  quarter += QEM[(lastAB << 2) | ab];
23  lastAB = ab;
24  if (ab == 3) {
25    if (quarter > 1) steps++;
26    else if (quarter < -1) steps--;
27    quarter = 0;
28  }
29}
30
31bool buttonPressed() {
32  static bool lastState = HIGH;
33  static unsigned long lastChange = 0;
34  bool state = digitalRead(PIN_SW);
35  if (state != lastState && millis() - lastChange > 30) {
36    lastChange = millis();
37    lastState = state;
38    return state == LOW;
39  }
40  return false;
41}
42
43void drawMenu() {
44  int top = selected > COUNT - 2 ? COUNT - 2 : selected;   // görünen ilk satır / first visible row
45  for (byte row = 0; row < 2; row++) {
46    int i = top + row;
47    char mark = ' ';
48    if (i == selected) mark = editing ? '*' : '>';
49    char line[17];
50    snprintf(line, sizeof(line), "%c%-11s%4d", mark, NAMES[i], values[i]);
51    lcd.setCursor(0, row);
52    lcd.print(line);
53  }
54}
55
56void setup() {
57  pinMode(PIN_CLK, INPUT_PULLUP);
58  pinMode(PIN_DT, INPUT_PULLUP);
59  pinMode(PIN_SW, INPUT_PULLUP);
60  for (byte i = 0; i < COUNT; i++) pinMode(LED_PINS[i], OUTPUT);
61  lcd.init();
62  lcd.backlight();
63  lastAB = (digitalRead(PIN_CLK) << 1) | digitalRead(PIN_DT);
64  attachInterrupt(digitalPinToInterrupt(PIN_CLK), readEncoder, CHANGE);
65  attachInterrupt(digitalPinToInterrupt(PIN_DT), readEncoder, CHANGE);
66  drawMenu();
67}
68
69void loop() {
70  noInterrupts();
71  int s = steps;                              // biriken tıkları al / take the pending detents
72  steps = 0;
73  interrupts();
74
75  if (s != 0) {
76    if (editing) {
77      values[selected] = constrain(values[selected] + s * 5, 0, 255);   // tık başına 5 / 5 per detent
78      analogWrite(LED_PINS[selected], values[selected]);
79    } else {
80      selected = constrain(selected + s, 0, COUNT - 1);
81    }
82    drawMenu();
83  }
84
85  if (buttonPressed()) {                      // bas: düzenle ya da bitir / press: edit or finish
86    editing = !editing;
87    drawMenu();
88  }
89}

Al inicio de la línea seleccionada aparece el signo >, y en modo edición el signo *. El código del encoder es el mismo que en el ejemplo anterior; la única diferencia es que la ISR acumula el número de clics sin procesar (steps) en lugar de una posición, y loop lo lee y lo pone a cero. Así no se pierde ningún clic aunque loop esté ocupado un rato: los acumulados se procesan juntos en la siguiente vuelta. En modo edición cada clic cambia el valor en 5; para un ajuste más fino puedes bajar el paso a 1 o añadir una aceleración que aumente el paso cuando se gira rápido.

Para mostrar el mismo menú en una pantalla OLED solo tienes que cambiar la función drawMenu(). La versión siguiente dibuja la línea seleccionada en colores invertidos sobre una barra blanca. Incluye las librerías Adafruit SSD1306 y Adafruit GFX en lugar de la del LCD, elimina el objeto lcd y llama a display.begin() en lugar de lcd.init() dentro de setup().

Arduino (C++)
1// LCD menü kodunda değişecek kısımlar / parts to change in the LCD menu sketch
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4
5Adafruit_SSD1306 display(128, 64, &Wire, -1);
6// setup() içinde lcd.init() ve lcd.backlight() yerine / in setup() instead of lcd.init() and lcd.backlight():
7//   display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
8
9void drawMenu() {
10  display.clearDisplay();
11  display.setTextSize(1);
12  for (byte i = 0; i < COUNT; i++) {
13    int y = i * 16;
14    if (i == selected) {
15      display.fillRect(0, y, 128, 14, SSD1306_WHITE);   // seçili satır çubuğu / highlight bar
16      display.setTextColor(SSD1306_BLACK);
17    } else {
18      display.setTextColor(SSD1306_WHITE);
19    }
20    display.setCursor(4, y + 3);
21    display.print(NAMES[i]);
22    display.setCursor(98, y + 3);
23    display.print(values[i]);
24    if (i == selected && editing) display.print('*');   // düzenleme işareti / edit marker
25  }
26  display.display();
27}
Si no quieres que los ajustes se pierdan al cortarse la alimentación, escribe el valor en la EEPROM al salir del modo edición y léelo de nuevo al arrancar. La EEPROM admite un número limitado de escrituras, así que guarda solo al pulsar el botón y no en cada clic. Usar una pulsación larga como orden de "volver" también es una forma sencilla de crear menús más profundos.

Encoder rotativo con ESP32: el contador por hardware PCNT

El ESP32 tiene un bloque de hardware llamado PCNT (Pulse Counter) que cuenta pulsos sin ocupar el procesador en absoluto. Cada unidad tiene dos canales, por lo que puede decodificar directamente la señal A/B; el ESP32 clásico tiene 8 unidades PCNT y el ESP32-S2 y el S3 tienen 4. El contador es de 16 bits e incluye un filtro de glitches que elimina el ruido corto. Este filtro se puede ajustar como máximo a unos 12,8 microsegundos, así que solo absorbe picos muy cortos, no todo el rebote. El conteo de cuatro flancos compensa por sí solo el resto del rebote: los rebotes de ida y vuelta se anulan como +1 y −1. El código siguiente usa el driver de ESP-IDF incluido en la versión 3.x del núcleo Arduino ESP32.

Arduino (C++)
1#include "driver/pulse_cnt.h"
2#include "driver/gpio.h"
3
4// KY-040: CLK -> GPIO 25, DT -> GPIO 26, SW -> GPIO 27, + -> 3V3
5const int PIN_CLK = 25;
6const int PIN_DT  = 26;
7const int PIN_SW  = 27;
8
9pcnt_unit_handle_t unit = NULL;
10
11void setup() {
12  Serial.begin(115200);
13  pinMode(PIN_SW, INPUT_PULLUP);
14
15  pcnt_unit_config_t unitCfg = {};
16  unitCfg.low_limit = -10000;                 // sınıra varınca sayaç 0'a döner / the counter returns to 0 at a limit
17  unitCfg.high_limit = 10000;
18  ESP_ERROR_CHECK(pcnt_new_unit(&unitCfg, &unit));
19
20  pcnt_glitch_filter_config_t filterCfg = {};
21  filterCfg.max_glitch_ns = 1000;             // 1 µs'den kısa iğneleri yok say / ignore spikes shorter than 1 µs
22  ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(unit, &filterCfg));
23
24  pcnt_chan_config_t aCfg = {};
25  aCfg.edge_gpio_num = PIN_CLK;               // A kanalı: CLK kenarları, DT seviyesi / channel A: CLK edges, DT level
26  aCfg.level_gpio_num = PIN_DT;
27  pcnt_channel_handle_t chA = NULL;
28  ESP_ERROR_CHECK(pcnt_new_channel(unit, &aCfg, &chA));
29
30  pcnt_chan_config_t bCfg = {};
31  bCfg.edge_gpio_num = PIN_DT;                // B kanalı: DT kenarları, CLK seviyesi / channel B: DT edges, CLK level
32  bCfg.level_gpio_num = PIN_CLK;
33  pcnt_channel_handle_t chB = NULL;
34  ESP_ERROR_CHECK(pcnt_new_channel(unit, &bCfg, &chB));
35
36  // 4x çözme: iki hattın tüm kenarları sayılır / 4x decoding: every edge of both lines is counted
37  ESP_ERROR_CHECK(pcnt_channel_set_edge_action(chA, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE));
38  ESP_ERROR_CHECK(pcnt_channel_set_level_action(chA, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
39  ESP_ERROR_CHECK(pcnt_channel_set_edge_action(chB, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE));
40  ESP_ERROR_CHECK(pcnt_channel_set_level_action(chB, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE));
41
42  gpio_pullup_en((gpio_num_t)PIN_CLK);        // çıplak encoder için iç pull-up / internal pull-up for a bare encoder
43  gpio_pullup_en((gpio_num_t)PIN_DT);
44
45  ESP_ERROR_CHECK(pcnt_unit_enable(unit));
46  ESP_ERROR_CHECK(pcnt_unit_clear_count(unit));
47  ESP_ERROR_CHECK(pcnt_unit_start(unit));
48}
49
50int lastDetent = 0;
51
52void loop() {
53  int count = 0;
54  pcnt_unit_get_count(unit, &count);
55  int detent = count / 4;                     // 4 kenar = 1 tık / 4 edges = 1 detent
56  if (detent != lastDetent) {
57    lastDetent = detent;
58    Serial.printf("Tik / detent: %d  (ham / raw: %d)\n", detent, count);
59  }
60  if (digitalRead(PIN_SW) == LOW) {           // basınca sıfırla / reset on press
61    pcnt_unit_clear_count(unit);
62  }
63  delay(20);
64}

Los dos canales se configuran como imagen especular uno del otro: el canal A cuenta los flancos de CLK e invierte el sentido según el nivel de DT, y el canal B cuenta los flancos de DT respecto a CLK. Como resultado, el contador cambia 4 por clic. Al llegar al límite de ±10000 vuelve a cero; si necesitas un rango mayor, puedes acumular los desbordamientos con la opción flags.accum_count y puntos de vigilancia (watch points). El ESP32-C3 no tiene unidad PCNT; en esa placa usa el código de Arduino con interrupciones. Si usas la versión 2.x del núcleo Arduino ESP32 o quieres un código más corto, la librería ESP32Encoder usa el mismo hardware y hace el trabajo con sus funciones attachFullQuad() y getCount(). Si programas el ESP32 en MicroPython, la clase machine.Encoder de las versiones recientes (por ejemplo, 1.27) también usa PCNT por debajo. Si eres nuevo con el ESP32, lee primero qué es el ESP32.

Encoder rotativo con Raspberry Pi Pico y MicroPython

Todos los pines GPIO de la Pico pueden generar interrupciones, así que leer un encoder en MicroPython con Pin.irq() es fácil. El código siguiente usa exactamente la misma tabla de estados que el ejemplo de Arduino y convierte el encoder en un regulador de LED: cada clic cambia el brillo un 5 por ciento y al presionar el eje el LED se enciende o se apaga. Conecta CLK a GP16, DT a GP17, SW a GP18 y un LED con una resistencia de 220 Ω a GP15. Para instalar MicroPython, consulta el artículo qué es la Raspberry Pi Pico.

MicroPython
1from machine import Pin, PWM
2import time
3
4clk = Pin(16, Pin.IN, Pin.PULL_UP)     # A fazı / phase A
5dt = Pin(17, Pin.IN, Pin.PULL_UP)      # B fazı / phase B
6sw = Pin(18, Pin.IN, Pin.PULL_UP)      # buton, basınca 0 / button, 0 when pressed
7led = PWM(Pin(15))                     # LED + 220 ohm
8led.freq(1000)
9
10# İndeks: (eski AB << 2) | yeni AB / Index: (old AB << 2) | new AB
11QEM = (0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0)
12last_ab = (clk.value() << 1) | dt.value()
13quarter = 0
14position = 10                          # 0-20 tık, başlangıç %50 / 0-20 detents, start at 50 %
15
16def on_edge(pin):
17    global last_ab, quarter, position
18    ab = (clk.value() << 1) | dt.value()
19    quarter += QEM[(last_ab << 2) | ab]
20    last_ab = ab
21    if ab == 3:                        # tıka oturdu / settled in a detent
22        if quarter > 1 and position < 20:
23            position += 1
24        elif quarter < -1 and position > 0:
25            position -= 1
26        quarter = 0
27
28clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
29dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
30
31on = True
32shown = -1
33while True:
34    if sw.value() == 0:                # mile basıldı / shaft pressed
35        time.sleep_ms(20)              # sıçramayı bekle / wait out the bounce
36        if sw.value() == 0:
37            on = not on
38            shown = -1                 # yeniden yaz / force an update
39            while sw.value() == 0:     # bırakılmasını bekle / wait for release
40                time.sleep_ms(10)
41    if position != shown:
42        shown = position
43        led.duty_u16(shown * 65535 // 20 if on else 0)
44        print("Parlaklik / brightness: %d%%" % (shown * 5), "acik / on" if on else "kapali / off")
45    time.sleep_ms(10)

Comprobamos los límites del valor (0–20 clics) dentro de la función de interrupción, de modo que el bucle principal solo lee la variable y no hay condición de carrera en la que ambos lados escriban a la vez. Cómo se controla el brillo del LED lo encontrarás en el artículo qué es PWM. Si prefieres una solución lista, el módulo rotary_irq_rp2 del proyecto micropython-rotary trae límites, vuelta al inicio (wrap) e inversión del sentido. Para encoders de motor muy rápidos, el bloque PIO de la Pico puede contar sin cargar el procesador. Si eres nuevo en MicroPython, el artículo fundamentos de MicroPython es un buen punto de partida.

Tipos de encoder rotativo: mecánico, óptico y magnético

Tipo¿Cómo funciona?Resolución típica¿Dónde se usa?
Mecánico (KY-040, EC11)Disco de contactos y dos interruptores12–30 clics por vueltaPerillas de menú, control de volumen, paneles frontales
ÓpticoDisco ranurado, LED y fototransistorDe cientos a miles de pulsos por vueltaMáquinas CNC, articulaciones de robots, motores industriales
Magnético (efecto Hall)Disco imantado y sensores HallPocos pulsos por vuelta del eje del motor (p. ej. 7 u 11)Motores N20 y motorreductores DC con encoder
AbsolutoGenera un código distinto para cada posiciónPor ejemplo 12 bits (4096 posiciones) en el AS5600Brazos robóticos, medición de ángulos; conoce su posición al encender

El código de este artículo también funciona con otros encoders incrementales con salidas A/B. Por ejemplo, en un motorreductor DC con encoder, un encoder Hall de 11 pulsos por vuelta del eje del motor, el conteo de cuatro flancos y una reducción de 1:30 producen 11 × 4 × 30 = 1320 cuentas por vuelta del eje de salida. Cuando la frecuencia de pulsos sube a miles por segundo, el código con interrupciones se lleva una parte notable del tiempo del procesador, mientras que la unidad PCNT del ESP32 hace este conteo sin esfuerzo. Los encoders ópticos industriales suelen trabajar a 5–24 V y tienen salidas de colector abierto o diferenciales; comprueba siempre el tipo de salida y la tensión en la hoja de datos antes de conectarlos.

Ideas de proyectos con encoder rotativo

  • Control de volumen digital: Ajusta con el encoder el volumen de la música de un DFPlayer Mini y presiona el eje para pasar a la siguiente canción.
  • Lámpara selectora de color: Gira el tono de un anillo de LED WS2812B con el encoder y pulsa para pasar al modo de brillo.
  • Posicionamiento preciso: Haz que cada clic mueva un motor paso a paso un número fijo de pasos; construye un slider de cámara o una plataforma giratoria.
  • Temporizador de cocina: Ajusta el tiempo con el encoder, pulsa para iniciar la cuenta atrás y haz sonar un zumbador al terminar.
  • Mando para juegos: Usa el encoder como raqueta en Pong; en los proyectos de juegos con Arduino da un control más fino que un joystick.
  • Ajuste de hora y alarma: Configura la hora y la alarma de un módulo de reloj RTC con una sola perilla.
  • Panel de instrumento de prueba: Cambia en pasos finos una frecuencia PWM, el valor de un generador de señales o la temperatura objetivo de una estación de soldadura.

Errores frecuentes y cómo solucionarlos

ProblemaCausa probableSolución
Cuenta 2 o 4 por clicSe cuenta cada flanco por separado o los contactos rebotanDivide el número de flancos entre 4, usa una tabla de estados y añade condensadores de 10–100 nF en CLK y DT
Solo cuenta uno de cada dos clicsEncoder de 30 clics / 15 pulsos; cada clic es medio cicloDivide el valor de la librería entre 2 en vez de 4; en el código de tabla cambia if (ab == 3) por if (ab == 3 || ab == 0)
Sentido invertidoCLK y DT intercambiados o módulo distintoIntercambia los números de pin de CLK y DT en el código
Se pierden pasos al girar rápidoSe usa sondeo y hay delay en loopUsa interrupciones o el PCNT del ESP32 y mantén corta la ISR
El pulsador se activa soloPin SW al aire, sin pull-upUsa pinMode(SW, INPUT_PULLUP)
El contador salta al azarGND no común, cables largos o sueltosUne los GND, acorta los cables y revisa los contactos de la protoboard
fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit'Núcleo Arduino ESP32 2.x (primer error) o un ESP32-C3, que no tiene unidad PCNT (segundo error)Actualiza el núcleo a 3.x en el Gestor de placas o usa la librería ESP32Encoder; en el C3 usa el código con interrupciones
El LCD solo muestra cuadrosContraste o dirección I2C incorrectosGira el potenciómetro trasero y prueba la dirección 0x3F en vez de 0x27
Si quieres probar el código antes de montar el circuito, el simulador Wokwi tiene una pieza KY-040 lista: tras hacer clic en el encoder puedes girarlo clic a clic con las flechas del teclado y pulsar el botón con la barra espaciadora. Para empezar, consulta la guía de simulación con Wokwi.

Un encoder rotativo es una de las formas más baratas de dar a un proyecto una interfaz de usuario profesional: una sola perilla y una pantalla pueden gestionar decenas de ajustes. Empieza con el código de sondeo para entender la lógica, pasa luego a la versión con interrupciones o con PCNT y adapta el ejemplo de menú a tu propio proyecto. En la app BlueGrays encontrarás proyectos de control de servo con potenciómetro, LCD I2C, pulsadores y contador de 7 segmentos con esquemas y código paso a paso, y podrás mejorarlos fácilmente cambiando el potenciómetro o el pulsador por un encoder.

Preguntas frecuentes

¿Qué diferencia hay entre un encoder rotativo y un potenciómetro?

Un potenciómetro entrega una tensión analógica que depende de la posición del eje, su giro está limitado a unos 270–300 grados y se lee con analogRead. Un encoder rotativo gira sin fin y produce pulsos digitales en cada clic; informa del sentido y la cantidad de movimiento, no de la posición. El encoder es mejor para menús y ajustes por pasos, y el potenciómetro para ajustes sencillos ligados a la posición.

¿Por qué mi encoder rotativo cuenta dos pasos por clic o se salta pasos?

Contar de más suele deberse al rebote de los contactos o a contar cada flanco por separado; dividir los 4 flancos de cada clic entre 4 y usar una tabla de estados lo soluciona. Los pasos perdidos aparecen cuando llegan pulsos mientras loop está ocupado. Conecta CLK y DT a pines de interrupción, léelos con interrupciones y, si hace falta, añade condensadores de 10–100 nF, y el problema desaparece.

¿El encoder rotativo KY-040 funciona a 3,3 V?

Sí. El KY-040 solo tiene interruptores mecánicos y resistencias pull-up, así que funciona perfectamente también a 3,3 V. En el ESP32 y la Raspberry Pi Pico conecta siempre el pin + a 3V3; si lo alimentas con 5 V, las pull-up llevan CLK y DT a 5 V y se supera el límite de las entradas de 3,3 V.

¿Cuántos pulsos por vuelta da el KY-040?

Los módulos KY-040 habituales tienen 20 clics por vuelta, y las líneas A y B completan un ciclo entero en cada clic. Cada ciclo tiene cuatro flancos, así que se cuentan 80 flancos por vuelta y cada clic equivale a 18 grados. Algunos encoders tienen 30 clics y 15 pulsos; en ellos cada clic corresponde a medio ciclo.

¿Se puede leer un encoder rotativo sin interrupciones?

Sí, mediante sondeo (polling), comprobando continuamente el pin CLK dentro de loop, y para una perilla de menú que se gira despacio suele bastar. Pero si loop tiene delay, actualizaciones de pantalla o cálculos largos, los clics rápidos se pierden. Para un resultado fiable se recomiendan las interrupciones y, en el ESP32, el contador por hardware PCNT.

¿Puedo conectar varios encoders rotativos a un Arduino?

Sí. Como en el Uno solo D2 y D3 tienen interrupción por hardware, con la librería Encoder puedes conectar el primer pin de cada encoder (el que se pasa primero al constructor) a un pin de interrupción y el otro a un pin normal, o usar interrupciones por cambio de pin (pin change interrupt). El Mega tiene seis pines de interrupción, el ESP32 clásico puede contar 8 encoders por hardware con sus 8 unidades PCNT y en la Pico cualquier GPIO puede generar una interrupción.

Ver todos los proyectos
Encender y apagar un LED con un botón
#10Principiante

Encender y apagar un LED con un botón

Pulsas el botón una vez y la luz se enciende, lo pulsas otra vez y se apaga, igual que el interruptor de tu habitación. El Arduino nota cada pulsación y cambia el LED al estado contrario, esperando un

4.8(100)
162
Control de LED con botón en ESP32 (Toggle)
#105Principiante

Control de LED con botón en ESP32 (Toggle)

Cada vez que se pulsa el botón, cambia el estado del LED: si está encendido se apaga, si está apagado se enciende. Patrón de conmutación clásico. Con debounce limpiamos el ruido mecánico del pulsador para garantizar una sola conmutación con una sola pulsación.

4.6(39)
57
Raspberry Pi Pico Botón Debounce: IRQ vs Software Polling (GP5 + GP4)
#159Intermedio
Premium

Raspberry Pi Pico Botón Debounce: IRQ vs Software Polling (GP5 + GP4)

¿Una pulsación y el código cuenta 3-4? ¡Es contact bounce! 🔁 Compara 2 métodos de conteo en Serial: polling RAW (captura bounce, cuenta mal) vs IRQ + software debounce (preciso). Aprende física del bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handlers, seguridad de variables globales en ISR.

0.0(0)
12
Funcionamiento del servomotor con potenciómetro
#36Principiante
Premium

Funcionamiento del servomotor con potenciómetro

A medida que giras el mando del potenciómetro, el brazo del servomotor gira con él: un extremo del mando equivale a cero grados y el otro a media vuelta. El Arduino lee sin parar la posición del mando

4.6(37)
52
Brillo de LED con Potenciómetro en ESP32 (Analógico → PWM)
#115Intermedio
Premium

Brillo de LED con Potenciómetro en ESP32 (Analógico → PWM)

En este proyecto controlamos el brillo de un LED de forma analógica mediante un potenciómetro. El pin ADC del ESP32 lee la tensión de 0-3,3 V procedente del potenciómetro y este valor se convierte en una señal PWM (modulación por ancho de pulso) que se envía al LED. Al girar el potenciómetro, el LED se enciende y se apaga gradualmente. Con Arduino Core 3.0+ se usa directamente `analogWrite()`; en el ESP32-S2/S3, la resolución del ADC es de 12 bits.

4.5(19)
32
Contador con display de 7 segmentos y botones
#43Avanzado
Premium

Contador con display de 7 segmentos y botones

Un contador sencillo con tres botones: con uno el número de la pantalla sube de uno en uno, con otro baja, y el tercero lo devuelve a cero. La pantalla de 7 segmentos de un solo dígito cuenta del 0 al

4.6(35)
47
Uso de la pantalla LCD con I2C
#32Principiante
Premium

Uso de la pantalla LCD con I2C

Dale a tu Arduino su propia pantalla y haz que escriba su primer mensaje. La pantalla LCD de dos líneas se conecta con solo unos pocos cables; primero aparece un saludo en la línea de arriba, luego un

4.8(53)
70
Juego de Raquetas - SSD1306
#86Intermedio
Premium

Juego de Raquetas - SSD1306

Una pelota, dos palas y tú: juegas al tenis de mesa, uno de los primeros videojuegos de la historia, en una pequeña pantalla OLED. Con dos botones mueves tu pala de la derecha arriba y abajo, mientras

4.5(47)
65