Encoder rotativo Arduino: KY-040, interrupt e menu su LCD
Encoder rotativo con Arduino: collegamento del KY-040, segnali A/B in quadratura, interrupt senza perdere passi, antirimbalzo, menu LCD, ESP32 PCNT e Pico.
Un encoder rotativo è una manopola di regolazione digitale per progetti Arduino che gira all'infinito, conta un passo a ogni scatto e funziona come un pulsante quando premi il suo albero. Collegato ad Arduino, l'encoder rotativo si comporta come la manopola del menu di una stampante 3D, il selettore di un oscilloscopio o la manopola del volume di un'auto: sa di quanti scatti hai girato e in quale verso, e non arriva mai a un fine corsa. Il modello che troverai più spesso nei kit è il modulo KY-040 a cinque pin.
In questa guida imparerai in cosa il KY-040 differisce da un potenziometro, come si ricava il verso di rotazione dalle fasi A e B (quadratura), come filtrare il rimbalzo dei contatti via hardware e software e come contare ogni singolo scatto senza perderne nessuno grazie agli interrupt. Poi costruiremo passo passo la scorciatoia con la libreria Encoder, la navigazione nei menu su LCD e OLED, il contatore hardware PCNT dell'ESP32 e il codice MicroPython per il Raspberry Pi Pico.
Cos'è un encoder rotativo e come funziona?
All'interno di un encoder meccanico come il KY-040 ci sono un disco di contatto dentato che gira con l'albero e due piccoli interruttori che vi strisciano sopra. Mentre l'albero gira, i due interruttori si aprono e si chiudono uno dopo l'altro e sui pin CLK (A) e DT (B) compaiono impulsi a onda quadra. Le due onde sono sfasate di un quarto di periodo, cioè di 90 gradi; si parla di segnale in quadratura. Il microcontrollore ricava il verso guardando quale onda cambia per prima, e l'entità della rotazione contando i cambiamenti. L'encoder non ricorda la sua posizione, segnala solo il movimento. Per questo il KY-040 è un encoder incrementale: all'accensione la posizione è zero e tutto il conteggio spetta al tuo codice.
| Caratteristica | Valore del KY-040 |
|---|---|
| Tipo | Meccanico, incrementale, uscite A/B |
| Scatti | 20 scatti per giro (30 su alcuni encoder) |
| Impulsi | 20 cicli A/B completi per giro, cioè 80 fronti |
| Risoluzione angolare | 18 gradi per scatto |
| Pin | CLK (A), DT (B), SW (pulsante), + (alimentazione), GND |
| Alimentazione | 3,3–5 V (alimenta solo le resistenze di pull-up) |
| Resistenze integrate | Pull-up da 10 kΩ su CLK e DT; la resistenza di SW non è montata su tutti i moduli |
| Pulsante | Normalmente aperto; premendo l'albero SW si collega a GND (attivo LOW) |
| Rotazione | Illimitata, senza fine corsa |
Encoder rotativo o potenziometro: qual è la differenza?
A prima vista un encoder somiglia molto a un potenziometro: entrambi hanno un albero rotante ed entrambi servono a regolare un valore. Ma funzionano in modo completamente diverso. Un potenziometro è un partitore di tensione; in base alla posizione dell'albero fornisce una tensione analogica tra 0 e 5 V, che leggi con analogRead. Un encoder invece produce impulsi e non una tensione; non ti dice la posizione, ma il cambiamento.

| Caratteristica | Potenziometro | Encoder rotativo (KY-040) |
|---|---|---|
| Rotazione | Circa 270–300 gradi, limitata | Illimitata, 360 gradi continui |
| Uscita | Tensione analogica (0–5 V) | Impulsi digitali (A e B) |
| Lettura | analogRead (ADC) | digitalRead o conteggio con interrupt |
| Posizione | Assoluta; la posizione dell'albero dà il valore anche dopo un'interruzione di corrente | Relativa; all'accensione la posizione è sconosciuta |
| Sensazione di scatto | Nessuna, gira in modo fluido | Sì, di solito 20 scatti per giro |
| Pulsante | No | Sì (premendo l'albero) |
| Problema tipico | Fruscii e valori instabili col tempo | Rimbalzo dei contatti (si risolve via software) |
| Uso tipico | Volume, angolo del servo, regolazione analogica | Navigazione nei menu, regolazioni numeriche a passi, pannelli di strumenti |
In breve, se il valore deve corrispondere esattamente alla posizione fisica dell'albero (muovere a mano il braccio di un servo o un semplice controllo del volume), il potenziometro è più semplice. Se vuoi scorrere un menu, aumentare e diminuire un numero a passi precisi, cambiare più impostazioni con una sola manopola o modificare il valore anche dal software, l'encoder è molto più flessibile. Per esempio, se un valore che indica 50 viene portato a 80 dal telefono, girando l'encoder riparti semplicemente da 80; con un potenziometro la posizione della manopola e il valore sul display non corrispondono più.
Fasi A e B: come funziona la lettura in quadratura
Quando il KY-040 è fermo su uno scatto, entrambi gli interruttori sono aperti e, grazie alle resistenze di pull-up, CLK e DT leggono entrambi HIGH (1). Quando giri la manopola di uno scatto, le due linee scendono a LOW una dopo l'altra e poi tornano a HIGH una dopo l'altra. In senso orario scende prima CLK, in senso antiorario prima DT. Questa sequenza di quattro stati è un codice Gray: a ogni passo cambia un solo bit. La tabella mostra cosa leggono i due pin durante uno scatto di movimento:
| Passo | Senso orario (CLK, DT) | Senso antiorario (CLK, DT) |
|---|---|---|
| Fermo su uno scatto | 1, 1 | 1, 1 |
| 1ª transizione | 0, 1 | 1, 0 |
| 2ª transizione | 0, 0 | 0, 0 |
| 3ª transizione | 1, 0 | 0, 1 |
| 4ª transizione (scatto successivo) | 1, 1 | 1, 1 |
Dalla tabella derivano due regole semplici. Prima: se DT è ancora HIGH quando CLK scende, la rotazione è oraria; se DT è già LOW, è antioraria. Seconda: uno scatto ha quattro transizioni, quindi 20 scatti per giro significano 80 transizioni. Il codice semplice usa solo la prima regola e conta una volta per scatto. Il codice robusto segue tutte e quattro le transizioni e così può scartare i movimenti con rimbalzi o lasciati a metà. Quale verso conti come orario può variare a seconda del modulo e del cablaggio; se il tuo contatore va al contrario, basta scambiare i pin CLK e DT nel codice.
Pinout del KY-040 e collegamento
| Pin del KY-040 | Funzione | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Fase A | D2 | GPIO 25 | GP16 |
| DT | Fase B | D3 | GPIO 26 | GP17 |
| SW | Pulsante (LOW se premuto) | D4 | GPIO 27 | GP18 |
| + | Alimentazione | 5V | 3V3 | 3V3(OUT) |
| GND | Massa | GND | GND | GND |
Su Arduino Uno e Nano gli interrupt hardware sono disponibili solo sui pin D2 e D3, quindi conviene collegare lì CLK e DT. Sul Mega i pin di interrupt sono 2, 3, 18, 19, 20 e 21; sul Leonardo 0, 1, 2, 3 e 7. Su ESP32 e Pico quasi ogni GPIO può generare un interrupt. Puoi controllare la piedinatura della tua scheda nello strumento Pinout. Nel codice usa INPUT_PULLUP per il pin SW: alcuni moduli non hanno una resistenza su questa linea e un pin flottante viene letto come se fosse premuto a caso. Sull'ESP32 i GPIO 34–39 sono solo ingressi e non hanno pull-up interno; non usarli per SW.
Prima prova passo passo: lettura in polling
- Collega il pin CLK del KY-040 a D2, DT a D3 e SW a D4.
- Collega il pin + a 5V e GND al GND di Arduino.
- Incolla il codice qui sotto nell'Arduino IDE e caricalo sulla scheda.
- Apri il Monitor seriale e imposta la velocità a 115200 baud.
- Gira la manopola di qualche scatto in entrambi i versi, poi premi l'albero per azzerare il contatore.
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}A ogni giro di loop il codice legge CLK e rileva solo il momento in cui CLK passa da HIGH a LOW. Se in quell'istante DT è ancora HIGH, la manopola ha girato in senso orario; se è LOW, in senso antiorario. È il metodo più facile da capire, ma ha due punti deboli. Primo, gli impulsi che arrivano mentre loop è occupato con altro (aggiornare un display, delay, leggere sensori) vanno persi, e il contatore resta indietro se giri velocemente. Secondo, i contatti meccanici rimbalzano: CLK può passare più volte da LOW a HIGH mentre scende una volta sola, e il codice può contare uno scatto come due o tre. Le due sezioni seguenti risolvono entrambi i problemi.
Rimbalzo dei contatti (antirimbalzo): soluzioni hardware e software
Le molle metalliche dentro l'encoder rimbalzano alcune volte quando toccano il contatto, prima di assestarsi. Nei datasheet dei produttori questo tempo è indicato di solito in pochi millisecondi (per esempio al massimo 2 ms). Con i pulsanti risolvi il rimbalzo dicendo "aspetta 20–50 ms dopo l'ultimo cambiamento", ma con un encoder non funziona: quando lo giri velocemente ci sono solo pochi millisecondi tra due fronti, e un'attesa lunga si mangia anche passi veri. Si usano invece questi metodi:
- Filtro hardware: Metti un condensatore da 10–100 nF tra CLK e GND e un altro tra DT e GND. Insieme ai pull-up da 10 kΩ del modulo formano un filtro RC; la costante di tempo va da 10 kΩ × 10 nF = 0,1 ms a 10 kΩ × 100 nF = 1 ms e assorbe i rimbalzi brevi. Puoi calcolarla con lo strumento costante di tempo RC e decifrare un condensatore marcato 103 o 104 con lo strumento codice dei condensatori.
- Tabella degli stati: A ogni fronte delle due linee si confrontano lo stato AB precedente e quello nuovo. Un rimbalzo significa andare avanti e indietro tra gli stessi due stati, il che produce alternativamente +1 e −1, che si annullano. Le transizioni in cui cambiano entrambi i bit contemporaneamente sono considerate non valide e vengono ignorate.
- Decidere solo sullo scatto: I quarti di passo vengono accumulati e il contatore aumenta o diminuisce solo quando l'encoder si ferma su uno scatto (entrambe le linee HIGH). Così un contatto che vibra sullo scatto non fa saltare il cursore del menu.
- Blocco temporale per il pulsante: Il pin SW si comporta come un normale pulsante; basta un blocco di 30 ms con millis().
Lettura con gli interrupt per non perdere nessun passo
Un interrupt fa sì che il processore interrompa ciò che sta facendo appena un pin cambia ed esegua una breve funzione (ISR). Collegando le linee dell'encoder agli interrupt, nessun fronte va perso anche se in loop c'è un delay di 50 ms o il display si aggiorna lentamente. Il codice seguente viene eseguito su entrambi i fronti di CLK e DT, usa la tabella degli stati descritta sopra e conta solo quando la manopola si ferma su uno scatto. Per le basi, consulta anche l'articolo interrupt su 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 codice | Cosa fa? |
|---|---|
| Tabella QEM[16] | Trasforma lo stato AB precedente e quello nuovo in +1, −1 o 0; le transizioni non valide in cui cambiano entrambe le linee contano 0 |
| attachInterrupt(..., CHANGE) | Esegue readEncoder() sia sul fronte di salita sia su quello di discesa di CLK e DT |
| quarter | Accumula i quarti di passo tra due scatti; le coppie +1 e −1 dovute ai rimbalzi si annullano |
| if (ab == 3) | Si decide solo quando l'encoder è fermo su uno scatto (entrambe le linee HIGH); un accumulo di +2 o più conta come uno scatto |
| volatile long position | La variabile condivisa da ISR e loop; volatile impedisce al compilatore di usare una copia non aggiornata |
| noInterrupts() / interrupts() | Impedisce alla ISR di intromettersi mentre si copia il long da 4 byte |
| buttonPressed() | Antirimbalzo di SW con blocco di 30 ms; restituisce true una sola volta, nel momento della pressione |
La scorciatoia: la libreria Encoder
Se non vuoi scrivere la tua ISR, la libreria Encoder di Paul Stoffregen fa lo stesso lavoro con codice ottimizzato. Apri il Gestore librerie dell'Arduino IDE, cerca "Encoder" e installa la libreria di Paul Stoffregen; per i passaggi di installazione dai un'occhiata alla guida come aggiungere una libreria Arduino. La libreria offre le prestazioni migliori quando entrambi i pin sono pin di interrupt, buone quando lo è solo il primo pin passato al costruttore e basse quando non lo è nessuno. Poiché conta ogni fronte, il valore cambia di 4 a ogni scatto; dividi per 4 per ottenere il numero di scatti.
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() restituisce il numero totale di fronti fino a quel momento, write(0) azzera il contatore e readAndReset() legge e azzera in un colpo solo. Passiamo al costruttore prima DT e poi CLK per il segno del verso di rotazione: in quest'ordine il senso orario conta in positivo. Se a te risulta invertito, scambia 2 e 3. La libreria attiva da sola anche le resistenze di pull-up interne dei pin.
Navigazione nei menu con encoder rotativo su LCD

I menu sono il campo in cui gli encoder si usano di più: giri la manopola per spostarti tra le righe, la premi per mettere l'impostazione selezionata in modalità modifica, giri per cambiare il valore e premi di nuovo per tornare al menu. L'esempio seguente regola i canali rosso, verde e blu di un LED RGB tra 0 e 255 e mostra i valori su un display LCD I2C. Collega l'LCD ad A4 (SDA) e A5 (SCL) e i terminali di colore di un LED RGB a catodo comune a D9, D10 e D11 tramite resistenze da 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}All'inizio della riga selezionata compare il segno >, in modalità modifica il segno *. Il codice dell'encoder è lo stesso dell'esempio precedente; l'unica differenza è che la ISR accumula il numero di scatti non ancora elaborati (steps) invece di una posizione, e loop lo legge e lo azzera. Così nessuno scatto va perso anche se loop resta occupato per un po': quelli accumulati vengono elaborati insieme al giro successivo. In modalità modifica ogni scatto cambia il valore di 5; per una regolazione più fine puoi ridurre il passo a 1 o aggiungere un'accelerazione che aumenta il passo quando giri velocemente.
Per mostrare lo stesso menu su un display OLED basta cambiare la funzione drawMenu(). La versione seguente disegna la riga selezionata a colori invertiti su una barra bianca. Includi le librerie Adafruit SSD1306 e Adafruit GFX al posto di quella dell'LCD, elimina l'oggetto lcd e in setup() chiama display.begin() invece di lcd.init().
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: il contatore hardware PCNT
L'ESP32 ha un blocco hardware chiamato PCNT (Pulse Counter) che conta gli impulsi senza impegnare affatto il processore. Ogni unità ha due canali e può quindi decodificare direttamente il segnale A/B; l'ESP32 classico ha 8 unità PCNT, l'ESP32-S2 e l'S3 ne hanno 4. Il contatore è a 16 bit e include un filtro antidisturbo (glitch filter) che elimina i disturbi brevi. Questo filtro si può impostare al massimo a circa 12,8 microsecondi, quindi assorbe solo picchi molto brevi e non tutto il rimbalzo. Il conteggio a quattro fronti compensa da solo il resto: i rimbalzi avanti e indietro si annullano come +1 e −1. Il codice seguente usa il driver ESP-IDF incluso nella versione 3.x del core 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}I due canali sono configurati in modo speculare: il canale A conta i fronti di CLK e inverte il verso in base al livello di DT, il canale B conta i fronti di DT rispetto a CLK. Il risultato è che il contatore cambia di 4 a ogni scatto. Quando raggiunge il limite di ±10000 torna a zero; se ti serve un intervallo più ampio, puoi accumulare gli overflow con l'opzione flags.accum_count e i punti di controllo (watch point). L'ESP32-C3 non ha un'unità PCNT; su quella scheda usa il codice Arduino con interrupt. Se usi la versione 2.x del core Arduino ESP32 o vuoi un codice più corto, la libreria ESP32Encoder sfrutta lo stesso hardware e fa il lavoro con le funzioni attachFullQuad() e getCount(). Se programmi l'ESP32 in MicroPython, anche la classe machine.Encoder delle versioni recenti (per esempio 1.27) usa il PCNT dietro le quinte. Se sei alle prime armi con l'ESP32, leggi prima Cos'è l'ESP32.
Encoder rotativo con Raspberry Pi Pico e MicroPython
Ogni pin GPIO del Pico può generare un interrupt, quindi leggere un encoder in MicroPython con Pin.irq() è semplice. Il codice seguente usa esattamente la stessa tabella degli stati dell'esempio Arduino e trasforma l'encoder in un dimmer per LED: ogni scatto cambia la luminosità del 5 per cento e premendo l'albero il LED si accende o si spegne. Collega CLK a GP16, DT a GP17, SW a GP18 e un LED con una resistenza da 220 Ω a GP15. Per installare MicroPython consulta l'articolo Cos'è il 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)Controlliamo i limiti del valore (0–20 scatti) dentro la funzione di interrupt, così il ciclo principale si limita a leggere la variabile e non si crea una race condition in cui entrambe le parti scrivono nello stesso momento. Come si regola la luminosità del LED lo trovi nell'articolo Cos'è il PWM. Se preferisci una soluzione pronta, il modulo rotary_irq_rp2 del progetto micropython-rotary offre limiti, ritorno all'inizio (wrap) e inversione del verso. Per encoder di motori molto veloci, il blocco PIO del Pico può contare senza impegnare il processore. Se sei nuovo di MicroPython, l'articolo basi di MicroPython è un buon punto di partenza.
Tipi di encoder rotativo: meccanico, ottico e magnetico
| Tipo | Come funziona? | Risoluzione tipica | Dove si usa? |
|---|---|---|---|
| Meccanico (KY-040, EC11) | Disco di contatto e due interruttori | 12–30 scatti per giro | Manopole dei menu, controlli del volume, pannelli frontali |
| Ottico | Disco forato, LED e fototransistor | Da centinaia a migliaia di impulsi per giro | Macchine CNC, giunti robotici, motori industriali |
| Magnetico (effetto Hall) | Disco magnetico e sensori Hall | Pochi impulsi per giro dell'albero motore (es. 7 o 11) | Motori N20 e motoriduttori DC con encoder |
| Assoluto | Genera un codice diverso per ogni posizione | Per esempio 12 bit (4096 posizioni) nell'AS5600 | Bracci robotici, misura di angoli; conosce la posizione all'accensione |
Il codice di questo articolo funziona anche con altri encoder incrementali con uscite A/B. Per esempio, su un motoriduttore DC con encoder, un encoder Hall da 11 impulsi per giro dell'albero motore, il conteggio a quattro fronti e un rapporto di riduzione di 1:30 producono 11 × 4 × 30 = 1320 conteggi per giro dell'albero di uscita. Quando la frequenza degli impulsi sale a migliaia al secondo, il codice con interrupt occupa una parte notevole del tempo del processore, mentre l'unità PCNT dell'ESP32 esegue questo conteggio senza sforzo. Gli encoder ottici industriali funzionano per lo più a 5–24 V e hanno uscite open collector o differenziali; prima di collegarli controlla sempre nel datasheet il tipo di uscita e la tensione.
Idee di progetto con encoder rotativo
- Manopola del volume digitale: Regola con l'encoder il volume della musica di un DFPlayer Mini e premi l'albero per passare al brano successivo.
- Lampada selettore di colore: Ruota la tonalità di un anello di LED WS2812B con l'encoder e premi per passare alla modalità luminosità.
- Posizionamento preciso: Fai avanzare un motore passo-passo di un numero fisso di passi a ogni scatto; costruisci uno slider per fotocamera o un piatto rotante.
- Timer da cucina: Imposta il tempo con l'encoder, premi per avviare il conto alla rovescia e fai suonare un buzzer alla fine.
- Controller per giochi: Usa l'encoder come racchetta in Pong; nei progetti di giochi con Arduino offre un controllo più fine di un joystick.
- Regolazione di ora e sveglia: Imposta l'ora e la sveglia di un modulo orologio RTC con un'unica manopola.
- Pannello per strumenti di prova: Cambia a piccoli passi una frequenza PWM, il valore di un generatore di segnali o la temperatura obiettivo di una stazione saldante.
Errori comuni e soluzioni
| Problema | Causa probabile | Soluzione |
|---|---|---|
| Conta 2 o 4 per scatto | Ogni fronte viene contato separatamente o i contatti rimbalzano | Dividi il numero di fronti per 4, usa una tabella degli stati, aggiungi condensatori da 10–100 nF su CLK e DT |
| Conta solo uno scatto su due | Encoder da 30 scatti / 15 impulsi; ogni scatto è mezzo ciclo | Dividi il valore della libreria per 2 invece che per 4; nel codice a tabella cambia if (ab == 3) in if (ab == 3 || ab == 0) |
| Verso invertito | CLK e DT scambiati o modulo diverso | Scambia i numeri di pin di CLK e DT nel codice |
| Si perdono passi girando velocemente | Si usa il polling e c'è un delay in loop | Usa gli interrupt o il PCNT dell'ESP32, tieni corta la ISR |
| Il pulsante si attiva da solo | Pin SW flottante, senza pull-up | Usa pinMode(SW, INPUT_PULLUP) |
| Il contatore salta a caso | GND non in comune, cavi lunghi o allentati | Metti in comune il GND, accorcia i cavi, controlla i contatti della breadboard |
| fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit' | Core Arduino ESP32 2.x (primo errore) oppure ESP32-C3, che non ha un'unità PCNT (secondo errore) | Aggiorna il core alla 3.x dal Gestore schede o usa la libreria ESP32Encoder; sul C3 usa il codice con interrupt |
| L'LCD mostra solo quadratini | Contrasto o indirizzo I2C errati | Gira il trimmer sul retro, prova l'indirizzo 0x3F invece di 0x27 |
Un encoder rotativo è uno dei modi più economici per dare a un progetto un'interfaccia utente professionale: una sola manopola e un display possono gestire decine di impostazioni. Parti dal codice in polling per capire la logica, poi passa alla versione con interrupt o con PCNT e adatta l'esempio di menu al tuo progetto. Nell'app BlueGrays trovi progetti di controllo servo con potenziometro, display LCD I2C, pulsanti e contatore a 7 segmenti con schemi e codice passo passo, e puoi migliorarli facilmente sostituendo il potenziometro o il pulsante con un encoder.
Domande frequenti
Qual è la differenza tra encoder rotativo e potenziometro?
Un potenziometro fornisce una tensione analogica che dipende dalla posizione dell'albero, la sua rotazione è limitata a circa 270–300 gradi e si legge con analogRead. Un encoder rotativo gira all'infinito e produce impulsi digitali a ogni scatto; indica il verso e l'entità del movimento, non la posizione. L'encoder è più adatto a menu e regolazioni a passi, il potenziometro a regolazioni semplici legate alla posizione.
Perché il mio encoder rotativo conta due passi per scatto o ne salta alcuni?
Contare troppo dipende di solito dal rimbalzo dei contatti o dal conteggio separato di ogni fronte; dividere per 4 i 4 fronti di ogni scatto e usare una tabella degli stati risolve il problema. I passi saltati compaiono quando gli impulsi arrivano mentre loop è occupato. Collega CLK e DT a pin di interrupt, leggili con gli interrupt e, se serve, aggiungi condensatori da 10–100 nF: il problema sparisce.
L'encoder rotativo KY-040 funziona a 3,3 V?
Sì. Il KY-040 è composto solo da interruttori meccanici e resistenze di pull-up, quindi funziona perfettamente anche a 3,3 V. Su ESP32 e Raspberry Pi Pico collega sempre il pin + a 3V3; se lo alimenti a 5 V, i pull-up portano CLK e DT a 5 V e si supera il limite degli ingressi a 3,3 V.
Quanti impulsi per giro dà il KY-040?
I moduli KY-040 più comuni hanno 20 scatti per giro, e le linee A e B compiono un ciclo completo a ogni scatto. Ogni ciclo ha quattro fronti, quindi si contano 80 fronti per giro e ogni scatto vale 18 gradi. Alcuni encoder hanno 30 scatti e 15 impulsi; in questi ogni scatto corrisponde a mezzo ciclo.
Si può leggere un encoder rotativo senza interrupt?
Sì, con il polling, cioè controllando continuamente il pin CLK dentro loop, e per una manopola di menu girata lentamente spesso basta. Ma se loop contiene delay, aggiornamenti del display o calcoli lunghi, gli scatti veloci vanno persi. Per un risultato affidabile si consigliano gli interrupt e, sull'ESP32, il contatore hardware PCNT.
Posso collegare più encoder rotativi a un Arduino?
Sì. Poiché sull'Uno solo D2 e D3 hanno interrupt hardware, con la libreria Encoder puoi collegare il primo pin di ogni encoder (quello passato per primo al costruttore) a un pin di interrupt e l'altro a un pin normale, oppure usare i pin change interrupt. Il Mega ha sei pin di interrupt, l'ESP32 classico può contare 8 encoder in hardware con le sue 8 unità PCNT e sul Pico ogni GPIO può generare un interrupt.
Progetti da realizzare dopo questo articolo
Vedi tutti i progetti
Accendere e spegnere un LED con un pulsante
Premi il pulsante una volta e la luce si accende, premilo di nuovo e si spegne, proprio come l'interruttore della tua stanza. L'Arduino si accorge di ogni pressione e porta il LED nello stato opposto,

Controllo LED ESP32 con pulsante (Toggle)
Ogni volta che si preme il pulsante, lo stato del LED cambia: se è acceso si spegne, se è spento si accende. Classico schema toggle. Con il debounce puliamo il rumore meccanico del pulsante per garantire una singola commutazione con una sola pressione.

Raspberry Pi Pico Pulsante Debounce: IRQ vs Software Polling (GP5 + GP4)
Una pressione e il codice conta 3-4? È contact bounce! 🔁 Confronta 2 metodi di conteggio in Serial: polling RAW (cattura bounce, conta male) vs IRQ + software debounce (preciso). Impara fisica del bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handler, sicurezza delle variabili globali ISR.

Funzionamento del servomotore con potenziometro
Mentre giri la manopola del potenziometro, il braccio del servomotore ruota insieme a lei: un'estremità della manopola corrisponde a zero gradi, l'altra a mezzo giro. L'Arduino legge di continuo la po

Luminosità di un LED con Potenziometro su ESP32 (Analogico → PWM)
In questo progetto controlliamo in modo analogico la luminosità di un LED tramite un potenziometro. Il pin ADC dell'ESP32 legge la tensione tra 0 e 3,3 V dal potenziometro; il valore viene convertito in un segnale PWM (Pulse Width Modulation) e inviato al LED. Ruotando il potenziometro, il LED si accende e si spegne gradualmente. Con Arduino Core 3.0+ si usa direttamente `analogWrite()`; su ESP32-S2/S3 la risoluzione dell'ADC è fissata a 12 bit.

Contatore con display a 7 segmenti e pulsanti
Un contatore semplice con tre pulsanti: con uno la cifra sul display sale di uno, con un altro scende di uno, e il terzo la riporta a zero. Il display a 7 segmenti a una sola cifra conta da 0 a 9, e q

Utilizzo dello schermo LCD con I2C
Regala al tuo Arduino uno schermo tutto suo e fagli scrivere il primo messaggio. Lo schermo LCD a due righe si collega con pochi fili; prima compare un saluto nella riga in alto, poi un secondo testo

Gioco delle Racchette - SSD1306
Una pallina, due racchette e tu: giochi a ping pong, uno dei primissimi videogiochi, su un piccolo display OLED. Con due pulsanti muovi su e giù la tua racchetta a destra, mentre il computer guida que
Altri articoli
App BlueGrays
Costruisci questo circuito passo passo sul telefono
Schema, codice ed elenco componenti in un’unica schermata. Oltre 160 progetti Arduino, ESP32 e Pico da realizzare.
Inquadra con il telefono
Punta la fotocamera sul codice: si apre lo store del tuo telefono.
oppure apri lo store

