Componenti · 15 min di lettura

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.

CaratteristicaValore del KY-040
TipoMeccanico, incrementale, uscite A/B
Scatti20 scatti per giro (30 su alcuni encoder)
Impulsi20 cicli A/B completi per giro, cioè 80 fronti
Risoluzione angolare18 gradi per scatto
PinCLK (A), DT (B), SW (pulsante), + (alimentazione), GND
Alimentazione3,3–5 V (alimenta solo le resistenze di pull-up)
Resistenze integratePull-up da 10 kΩ su CLK e DT; la resistenza di SW non è montata su tutti i moduli
PulsanteNormalmente aperto; premendo l'albero SW si collega a GND (attivo LOW)
RotazioneIllimitata, 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.

Potenziometro a confronto con un encoder rotativo
Potenziometro: la rotazione è limitata a circa 270–300 gradi e la posizione viene fornita come tensione analogica. Un encoder rotativo gira senza fermarsi e produce impulsi digitali.
CaratteristicaPotenziometroEncoder rotativo (KY-040)
RotazioneCirca 270–300 gradi, limitataIllimitata, 360 gradi continui
UscitaTensione analogica (0–5 V)Impulsi digitali (A e B)
LetturaanalogRead (ADC)digitalRead o conteggio con interrupt
PosizioneAssoluta; la posizione dell'albero dà il valore anche dopo un'interruzione di correnteRelativa; all'accensione la posizione è sconosciuta
Sensazione di scattoNessuna, gira in modo fluidoSì, di solito 20 scatti per giro
PulsanteNoSì (premendo l'albero)
Problema tipicoFruscii e valori instabili col tempoRimbalzo dei contatti (si risolve via software)
Uso tipicoVolume, angolo del servo, regolazione analogicaNavigazione 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:

PassoSenso orario (CLK, DT)Senso antiorario (CLK, DT)
Fermo su uno scatto1, 11, 1
1ª transizione0, 11, 0
2ª transizione0, 00, 0
3ª transizione1, 00, 1
4ª transizione (scatto successivo)1, 11, 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-040FunzioneArduino UnoESP32Raspberry Pi Pico
CLKFase AD2GPIO 25GP16
DTFase BD3GPIO 26GP17
SWPulsante (LOW se premuto)D4GPIO 27GP18
+Alimentazione5V3V33V3(OUT)
GNDMassaGNDGNDGND
Sull'ESP32 e sul Raspberry Pi Pico collega sempre il pin + del modulo a 3,3 V. Le resistenze di pull-up del KY-040 sono collegate al pin +, quindi se alimenti il modulo a 5 V, CLK e DT vengono portati a 5 V e si supera il limite degli ingressi a 3,3 V. Dato che l'encoder è fatto solo di interruttori, funziona perfettamente a 3,3 V.

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

  1. Collega il pin CLK del KY-040 a D2, DT a D3 e SW a D4.
  2. Collega il pin + a 5V e GND al GND di Arduino.
  3. Incolla il codice qui sotto nell'Arduino IDE e caricalo sulla scheda.
  4. Apri il Monitor seriale e imposta la velocità a 115200 baud.
  5. Gira la manopola di qualche scatto in entrambi i versi, poi premi l'albero per azzerare il contatore.
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}

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.

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 codiceCosa 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
quarterAccumula 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 positionLa 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
Mantieni la ISR il più breve possibile: non usare al suo interno Serial.print, delay o comandi del display; aggiorna solo il contatore e lascia il lavoro vero a loop. Dichiara volatile ogni variabile condivisa tra ISR e loop. Poiché l'Uno ha un processore a 8 bit, disattivare brevemente gli interrupt mentre leggi una variabile da 2 o 4 byte evita di leggere un valore aggiornato a metà.

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.

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() 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.

Display LCD I2C 16x2
Display LCD I2C 16x2: si collega con i soli fili SDA e SCL; le impostazioni dell'esempio di menu vengono mostrate su questo display.

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 Ω.

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}

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().

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}
Se non vuoi perdere le impostazioni quando manca la corrente, scrivi il valore nella EEPROM quando esci dalla modalità modifica e rileggilo all'avvio. La EEPROM sopporta un numero limitato di scritture, quindi salva solo alla pressione del pulsante e non a ogni scatto. Usare una pressione lunga come comando "indietro" è anche un modo semplice per creare menu più profondi.

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.

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}

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.

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)

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

TipoCome funziona?Risoluzione tipicaDove si usa?
Meccanico (KY-040, EC11)Disco di contatto e due interruttori12–30 scatti per giroManopole dei menu, controlli del volume, pannelli frontali
OtticoDisco forato, LED e fototransistorDa centinaia a migliaia di impulsi per giroMacchine CNC, giunti robotici, motori industriali
Magnetico (effetto Hall)Disco magnetico e sensori HallPochi impulsi per giro dell'albero motore (es. 7 o 11)Motori N20 e motoriduttori DC con encoder
AssolutoGenera un codice diverso per ogni posizionePer esempio 12 bit (4096 posizioni) nell'AS5600Bracci 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

ProblemaCausa probabileSoluzione
Conta 2 o 4 per scattoOgni fronte viene contato separatamente o i contatti rimbalzanoDividi 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 dueEncoder da 30 scatti / 15 impulsi; ogni scatto è mezzo cicloDividi 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 invertitoCLK e DT scambiati o modulo diversoScambia i numeri di pin di CLK e DT nel codice
Si perdono passi girando velocementeSi usa il polling e c'è un delay in loopUsa gli interrupt o il PCNT dell'ESP32, tieni corta la ISR
Il pulsante si attiva da soloPin SW flottante, senza pull-upUsa pinMode(SW, INPUT_PULLUP)
Il contatore salta a casoGND non in comune, cavi lunghi o allentatiMetti 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 quadratiniContrasto o indirizzo I2C erratiGira il trimmer sul retro, prova l'indirizzo 0x3F invece di 0x27
Se vuoi provare il codice prima di montare il circuito, il simulatore Wokwi ha un componente KY-040 già pronto: dopo aver cliccato sull'encoder puoi girarlo scatto per scatto con i tasti freccia e premere il pulsante con la barra spaziatrice. Per iniziare consulta la guida alla simulazione con Wokwi.

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.

Vedi tutti i progetti
Accendere e spegnere un LED con un pulsante
#10Principiante

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,

4.8(100)
162
Controllo LED ESP32 con pulsante (Toggle)
#105Principiante

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.

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

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.

0.0(0)
12
Funzionamento del servomotore con potenziometro
#36Principiante
Premium

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

4.6(37)
52
Luminosità di un LED con Potenziometro su ESP32 (Analogico → PWM)
#115Intermedio
Premium

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.

4.5(19)
32
Contatore con display a 7 segmenti e pulsanti
#43Avanzato
Premium

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

4.6(35)
47
Utilizzo dello schermo LCD con I2C
#32Principiante
Premium

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

4.8(53)
70
Gioco delle Racchette - SSD1306
#86Intermedio
Premium

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

4.5(47)
65