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ística | Valor del KY-040 |
|---|---|
| Tipo | Mecánico, incremental, salidas A/B |
| Clics (retenes) | 20 clics por vuelta (30 en algunos encoders) |
| Pulsos | 20 ciclos A/B completos por vuelta, es decir, 80 flancos |
| Resolución angular | 18 grados por clic |
| Pines | CLK (A), DT (B), SW (pulsador), + (alimentación), GND |
| Alimentación | 3,3–5 V (solo alimenta las resistencias pull-up) |
| Resistencias integradas | Pull-up de 10 kΩ en CLK y DT; la resistencia de SW no viene montada en todos los módulos |
| Pulsador | Normalmente abierto; al presionar el eje, SW se conecta a GND (activo en LOW) |
| Giro | Ilimitado, 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.

| Característica | Potenciómetro | Encoder rotativo (KY-040) |
|---|---|---|
| Giro | Unos 270–300 grados, limitado | Ilimitado, 360 grados continuos |
| Salida | Tensión analógica (0–5 V) | Pulsos digitales (A y B) |
| Lectura | analogRead (ADC) | digitalRead o conteo con interrupciones |
| Posición | Absoluta; la posición del eje da el valor incluso tras un corte de luz | Relativa; la posición es desconocida al encender |
| Sensación de clic | No, gira suave | Sí, normalmente 20 clics por vuelta |
| Pulsador | No | Sí (presionando el eje) |
| Problema típico | Ruido y valores que bailan con el tiempo | Rebote de contactos (se resuelve por software) |
| Uso típico | Volumen, ángulo de servo, ajuste analógico | Navegació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:
| Paso | Sentido horario (CLK, DT) | Sentido antihorario (CLK, DT) |
|---|---|---|
| En reposo en un clic | 1, 1 | 1, 1 |
| 1.ª transición | 0, 1 | 1, 0 |
| 2.ª transición | 0, 0 | 0, 0 |
| 3.ª transición | 1, 0 | 0, 1 |
| 4.ª transición (siguiente clic) | 1, 1 | 1, 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-040 | Función | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Fase A | D2 | GPIO 25 | GP16 |
| DT | Fase B | D3 | GPIO 26 | GP17 |
| SW | Pulsador (LOW al pulsar) | D4 | GPIO 27 | GP18 |
| + | Alimentación | 5V | 3V3 | 3V3(OUT) |
| GND | Tierra | GND | GND | GND |
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)
- Conecta el pin CLK del KY-040 a D2, DT a D3 y SW a D4.
- Conecta el pin + a 5V y GND al GND del Arduino.
- Pega el código siguiente en el Arduino IDE y súbelo a la placa.
- Abre el Monitor Serie y ajusta la velocidad a 115200 baudios.
- Gira la perilla unos clics en ambos sentidos y luego presiona el eje para poner el contador a cero.
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.
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 |
| quarter | Acumula 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 position | La 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 |
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.
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.
Navegación por menús con encoder rotativo en un LCD

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 Ω.
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().
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}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.
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.
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 interruptores | 12–30 clics por vuelta | Perillas de menú, control de volumen, paneles frontales |
| Óptico | Disco ranurado, LED y fototransistor | De cientos a miles de pulsos por vuelta | Máquinas CNC, articulaciones de robots, motores industriales |
| Magnético (efecto Hall) | Disco imantado y sensores Hall | Pocos pulsos por vuelta del eje del motor (p. ej. 7 u 11) | Motores N20 y motorreductores DC con encoder |
| Absoluto | Genera un código distinto para cada posición | Por ejemplo 12 bits (4096 posiciones) en el AS5600 | Brazos 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
| Problema | Causa probable | Solución |
|---|---|---|
| Cuenta 2 o 4 por clic | Se cuenta cada flanco por separado o los contactos rebotan | Divide 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 clics | Encoder de 30 clics / 15 pulsos; cada clic es medio ciclo | Divide 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 invertido | CLK y DT intercambiados o módulo distinto | Intercambia los números de pin de CLK y DT en el código |
| Se pierden pasos al girar rápido | Se usa sondeo y hay delay en loop | Usa interrupciones o el PCNT del ESP32 y mantén corta la ISR |
| El pulsador se activa solo | Pin SW al aire, sin pull-up | Usa pinMode(SW, INPUT_PULLUP) |
| El contador salta al azar | GND no común, cables largos o sueltos | Une 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 cuadros | Contraste o dirección I2C incorrectos | Gira el potenciómetro trasero y prueba la dirección 0x3F en vez de 0x27 |
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.
Proyectos para hacer después de leer esto
Ver todos los proyectos
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

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.

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.

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

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.

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

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

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
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 160 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

