Componentes · 16 min de leitura

Encoder rotativo com Arduino: ligação do KY-040, interrupções e código de menu

Como usar um encoder rotativo com Arduino: ligação do KY-040, decodificação em quadratura A/B, interrupções sem perder passos, debounce, menu em LCD, PCNT do ESP32 e Pico.

Um encoder rotativo é um botão de controle digital para projetos com Arduino que gira sem fim, conta um passo a cada clique e funciona como botão de pressão quando você aperta o eixo. Ligado a um Arduino, o encoder rotativo se comporta como o botão de menu de uma impressora 3D, o seletor de ajuste de um osciloscópio ou o botão de volume do carro: ele sabe quantos cliques você girou e em que direção, e nunca chega a um batente. O modelo que aparece na maioria dos kits de hobby é o módulo KY-040, de cinco pinos.

Neste guia você vai aprender como o KY-040 se diferencia de um potenciômetro, como a direção é decodificada a partir das fases A e B (quadratura), como filtrar o repique dos contatos no hardware e no software e como contar cada clique com interrupções. Depois, passo a passo, vamos usar o atalho da biblioteca Encoder, montar a navegação de menu em um LCD e em um OLED, usar o contador de hardware PCNT do ESP32 e escrever código MicroPython para o Raspberry Pi Pico.

O que é um encoder rotativo e como ele funciona?

Dentro de um encoder mecânico como o KY-040 há um disco de contato dentado que gira com o eixo e duas pequenas chaves que deslizam sobre ele. Conforme o eixo gira, as duas chaves abrem e fecham uma depois da outra, gerando pulsos de onda quadrada nos pinos CLK (A) e DT (B). As duas ondas ficam defasadas em um quarto de período, ou 90 graus; isso se chama sinal em quadratura. O microcontrolador descobre a direção verificando qual onda muda primeiro e a quantidade de rotação contando as mudanças. O encoder não guarda a posição, ele só informa o movimento. Por isso o KY-040 é um encoder incremental: a posição é zero ao ligar e a contagem fica inteiramente por conta do seu código.

CaracterísticaValor no KY-040
TipoMecânico, incremental, saídas A/B
Detentes20 cliques por volta (30 em alguns encoders)
Pulsos20 ciclos A/B completos por volta, ou seja, 80 bordas
Resolução angular18 graus por clique
PinosCLK (A), DT (B), SW (botão), + (alimentação), GND
Alimentação3,3–5 V (só alimenta os resistores de pull-up)
Resistores embutidosPull-ups de 10 kΩ em CLK e DT; o resistor do SW não vem em todos os módulos
BotãoNormalmente aberto; apertar o eixo liga SW ao GND (ativo em LOW)
RotaçãoIlimitada, sem batente

Encoder rotativo ou potenciômetro: qual é a diferença?

À primeira vista, um encoder se parece muito com um potenciômetro: os dois têm um eixo giratório e os dois servem para ajustar um valor. Mas funcionam de jeitos completamente diferentes. Um potenciômetro é um divisor de tensão; ele fornece uma tensão analógica entre 0 e 5 V conforme a posição do eixo, e você a lê com analogRead. Um encoder gera pulsos em vez de tensão; ele informa a mudança, não a posição.

Potenciômetro para comparação com um encoder rotativo
Potenciômetro: a rotação é limitada a cerca de 270–300 graus e a posição é informada como tensão analógica. Um encoder rotativo gira sem parar e gera pulsos digitais.
CaracterísticaPotenciômetroEncoder rotativo (KY-040)
RotaçãoCerca de 270–300 graus, limitadaIlimitada, 360 graus contínuos
SaídaTensão analógica (0–5 V)Pulsos digitais (A e B)
LeituraanalogRead (ADC)digitalRead ou contagem com interrupções
PosiçãoAbsoluta; a posição do eixo dá o valor mesmo depois de uma queda de energiaRelativa; a posição é desconhecida ao ligar
Sensação de cliqueNenhuma, gira suavementeSim, normalmente 20 cliques por volta
BotãoNãoSim (apertando o eixo)
Problema típicoChiado e valores que variam com o tempoRepique dos contatos (resolvido no software)
Uso típicoVolume, ângulo de servo, ajuste analógicoNavegação em menus, ajustes numéricos em passos, painéis de aparelhos

Resumindo: se o valor precisa corresponder um a um à posição física do eixo (mover o braço de um servo com a mão ou um controle de volume simples), o potenciômetro é mais simples. Se você quer percorrer um menu, aumentar e diminuir um número em passos precisos, mudar várias configurações com um só botão ou também alterar o valor pelo software, o encoder é muito mais flexível. Por exemplo, se um valor que mostra 50 é ajustado para 80 pelo celular, ao girar o encoder ele simplesmente continua a partir de 80; com um potenciômetro, a posição do botão e o valor na tela deixam de coincidir.

Fases A e B: como funciona a decodificação em quadratura

Quando o KY-040 está parado em um detente, as duas chaves estão abertas e, graças aos resistores de pull-up, CLK e DT leem HIGH (1). Quando você gira o botão um clique, as duas linhas caem para LOW uma depois da outra e depois voltam para HIGH uma depois da outra. No sentido horário, CLK cai primeiro; no anti-horário, DT cai primeiro. Essa sequência de quatro estados é um código Gray: só um bit muda a cada passo. A tabela abaixo mostra o que os dois pinos leem durante um clique de movimento:

PassoSentido horário (CLK, DT)Sentido anti-horário (CLK, DT)
Parado em um detente1, 11, 1
1ª transição0, 11, 0
2ª transição0, 00, 0
3ª transição1, 00, 1
4ª transição (próximo clique)1, 11, 1

Da tabela saem duas regras simples. Primeira: se DT ainda está em HIGH quando CLK cai, o botão está girando no sentido horário; se DT já está em LOW, está girando no anti-horário. Segunda: um clique tem quatro transições, então 20 cliques por volta significam 80 transições. O código simples usa só a primeira regra e conta uma vez por clique. O código robusto acompanha as quatro transições e assim consegue filtrar repiques ou movimentos pela metade. Qual direção conta como horária pode variar conforme o módulo e a ligação; se o seu contador andar ao contrário, basta trocar os pinos CLK e DT no código.

Pinagem e ligação do KY-040

Pino do KY-040FunçãoArduino UnoESP32Raspberry Pi Pico
CLKFase AD2GPIO 25GP16
DTFase BD3GPIO 26GP17
SWBotão (LOW quando pressionado)D4GPIO 27GP18
+Alimentação5V3V33V3(OUT)
GNDTerraGNDGNDGND
No ESP32 e no Raspberry Pi Pico, ligue sempre o pino + do módulo em 3,3 V. Os resistores de pull-up do KY-040 vão para o pino +, então, se você alimentar o módulo com 5 V, CLK e DT são puxados para 5 V e o limite das entradas de 3,3 V é ultrapassado. Como o encoder não passa de um conjunto de chaves, ele funciona perfeitamente em 3,3 V.

No Arduino Uno e no Nano, as interrupções de hardware só existem nos pinos D2 e D3, então o melhor é ligar CLK e DT nesses dois pinos. No Mega, os pinos de interrupção são 2, 3, 18, 19, 20 e 21; no Leonardo, 0, 1, 2, 3 e 7. No ESP32 e no Pico, quase todo GPIO pode disparar uma interrupção. Você pode conferir o mapa de pinos da sua placa na ferramenta Pinagem. Use INPUT_PULLUP para o pino SW no código: alguns módulos não têm resistor nessa linha, e um pino flutuante é lido como se fosse apertado aleatoriamente. No ESP32, os GPIO 34–39 são só de entrada e não têm pull-up interno, então não os use para o SW.

Primeiro teste passo a passo: leitura por polling

  1. Ligue o pino CLK do KY-040 no D2, o DT no D3 e o SW no D4.
  2. Ligue o pino + no 5V e o GND no GND do Arduino.
  3. Cole o código abaixo no Arduino IDE e carregue-o na placa.
  4. Abra o Monitor Serial e ajuste a velocidade para 115200 baud.
  5. Gire o botão alguns cliques nos dois sentidos e depois aperte o eixo para zerar o contador.
C++ (Arduino IDE)
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 cada passagem pelo loop, o código lê o CLK e só captura o momento em que o CLK cai de HIGH para LOW. Se nesse momento o DT ainda estiver em HIGH, o botão girou no sentido horário; se estiver em LOW, no anti-horário. É o método mais fácil de entender, mas tem dois pontos fracos. Primeiro, os pulsos que chegam enquanto o loop está ocupado com outra coisa (atualizar um display, delay, ler sensores) se perdem, e o contador fica para trás quando você gira rápido. Segundo, contatos mecânicos repicam: o CLK pode ir LOW-HIGH várias vezes ao cair uma única vez, e o código pode contar um clique como dois ou três. As duas próximas seções resolvem os dois problemas.

Repique dos contatos (debounce): soluções no hardware e no software

As molas metálicas dentro do encoder quicam algumas vezes ao tocar o contato antes de se acomodar. Os datasheets dos fabricantes costumam dar esse tempo como alguns milissegundos (por exemplo, 2 ms no máximo). Com botões de pressão você resolve o repique dizendo “espere 20–50 ms depois da última mudança”, mas isso não funciona com um encoder: quando ele é girado rápido, há só alguns milissegundos entre duas bordas, e uma espera longa engole também os passos reais. Em vez disso, usam-se estes métodos:

  • Filtro de hardware: coloque um capacitor de 10–100 nF entre CLK e GND e outro entre DT e GND. Junto com os pull-ups de 10 kΩ do módulo, eles formam um filtro RC; a constante de tempo fica entre 10 kΩ × 10 nF = 0,1 ms e 10 kΩ × 100 nF = 1 ms, o que engole os repiques curtos. Você pode calculá-la com a ferramenta constante de tempo RC e decifrar um capacitor marcado 103 ou 104 com a ferramenta código de capacitor.
  • Tabela de estados: a cada borda das duas linhas, os estados AB antigo e novo são comparados. Repicar significa pular para lá e para cá entre os mesmos dois estados, o que gera +1 e −1 alternadamente, e eles se anulam. As transições em que os dois bits mudam ao mesmo tempo são tratadas como inválidas e ignoradas.
  • Decidir só no detente: os quartos de passo são acumulados e o contador só aumenta ou diminui quando o encoder se acomoda em um detente (as duas linhas em HIGH). Assim, um contato tremendo em cima de um clique não faz o cursor do menu pular.
  • Bloqueio para o botão: o pino SW se comporta como um botão de pressão comum; um bloqueio de 30 ms com millis() é suficiente.

Leitura com interrupções para nunca perder um passo

Uma interrupção faz o processador largar o que está fazendo no instante em que um pino muda e executar uma função curta (ISR). Depois que as linhas do encoder são ligadas a interrupções, nenhuma borda se perde, mesmo que o loop tenha um delay de 50 ms ou que o display atualize devagar. O código abaixo roda nas duas bordas de CLK e DT, usa a tabela de estados descrita acima e só conta quando o botão se acomoda em um detente. Para o básico, veja também o artigo interrupções no Arduino.

C++ (Arduino IDE)
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 do códigoO que faz?
Tabela QEM[16]Converte o estado AB antigo e o novo em +1, −1 ou 0; transições inválidas, em que as duas linhas mudam ao mesmo tempo, contam como 0
attachInterrupt(..., CHANGE)Executa readEncoder() nas bordas de subida e de descida de CLK e DT
quarter (quarto de passo)Acumula os quartos de passo entre dois cliques; os pares +1 e −1 causados pelo repique se anulam
if (ab == 3)A decisão só é tomada quando o encoder se acomoda em um detente (as duas linhas em HIGH); um valor acumulado de +2 ou mais conta como um clique
volatile long position (variável compartilhada)A variável compartilhada pela ISR e pelo loop; volatile impede o compilador de usar uma cópia desatualizada
noInterrupts() / interrupts()Impede que a ISR interrompa no meio da cópia do long de 4 bytes
buttonPressed()Faz o debounce do SW com um bloqueio de 30 ms e retorna true só uma vez, no momento em que é pressionado
Mantenha a ISR o mais curta possível: não use Serial.print, delay nem comandos de display dentro dela; apenas atualize o contador e deixe o trabalho de verdade para o loop. Declare como volatile toda variável compartilhada pela ISR e pelo loop. Como o Uno tem processador de 8 bits, desativar as interrupções por um instante ao ler uma variável de 2 ou 4 bytes evita que você leia um valor atualizado pela metade.

O atalho: a biblioteca Encoder

Se você não quer escrever sua própria ISR, a biblioteca Encoder, de Paul Stoffregen, faz o mesmo trabalho com código otimizado. Abra o Gerenciador de Bibliotecas no Arduino IDE, busque “Encoder” e instale a biblioteca de Paul Stoffregen; para os passos de instalação, dê uma olhada no guia como instalar uma biblioteca no Arduino. A biblioteca tem o melhor desempenho quando os dois pinos são de interrupção, bom desempenho quando só o primeiro pino passado ao construtor é, e desempenho baixo quando nenhum é. Como ela conta cada borda, o valor muda 4 a cada clique; divida por 4 para obter o número de cliques.

C++ (Arduino IDE)
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() retorna o total de bordas até o momento, write(0) zera o contador e readAndReset() lê e zera de uma vez. Passamos o DT primeiro e o CLK depois ao construtor por causa do sinal da direção: nessa ordem, o sentido horário conta como positivo. Se no seu caso ficar invertido, troque o 2 e o 3. A biblioteca também ativa sozinha os resistores de pull-up internos dos pinos.

Display LCD 16x2 I2C
Display LCD 16x2 I2C: ele se liga com apenas os fios SDA e SCL; as configurações do exemplo de menu aparecem neste display.

Os menus são onde os encoders mais aparecem: você gira o botão para passar de uma linha a outra, aperta para colocar a configuração selecionada em modo de edição, gira para mudar o valor e aperta mais uma vez para voltar ao menu. O exemplo abaixo ajusta os canais vermelho, verde e azul de um LED RGB entre 0 e 255 e mostra os valores em um display LCD I2C. Ligue o LCD em A4 (SDA) e A5 (SCL), e as pernas de cor de um LED RGB de cátodo comum em D9, D10 e D11 por meio de resistores de 220 Ω.

C++ (Arduino IDE)
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}

Um sinal > aparece no início da linha selecionada, e um sinal * no modo de edição. O código do encoder é o mesmo do exemplo anterior; a única diferença é que a ISR acumula o número de cliques ainda não processados (steps) em vez de uma posição, e o loop lê e zera esse valor. Assim, mesmo que o loop fique ocupado por um tempo, nenhum clique se perde; os acumulados são tratados juntos na próxima passagem. No modo de edição, cada clique muda o valor em 5; para um controle mais fino, você pode reduzir o passo para 1 ou adicionar uma aceleração que aumenta o passo quando o botão é girado rápido.

Para mostrar o mesmo menu em um display OLED de 0,96 polegada, basta mudar a função drawMenu(). A versão abaixo desenha a linha selecionada com cores invertidas, sobre uma barra branca. Adicione as bibliotecas Adafruit SSD1306 e Adafruit GFX no lugar da biblioteca do LCD, apague o objeto lcd e chame display.begin() em vez de lcd.init() no setup().

C++ (Arduino IDE)
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 você não quer perder as configurações quando a energia cai, grave o valor na EEPROM ao sair do modo de edição e leia-o de volta na inicialização. A EEPROM tem um número limitado de ciclos de gravação, então salve só quando o botão for pressionado, não a cada clique. Usar um toque longo como comando de “voltar” também é um jeito fácil de montar menus mais profundos.

Encoder rotativo com o ESP32: o contador de hardware PCNT

O ESP32 tem um bloco de hardware chamado PCNT (Pulse Counter) que conta pulsos sem ocupar o processador. Cada unidade tem dois canais, então consegue decodificar o sinal A/B diretamente; o ESP32 clássico tem 8 unidades PCNT, e o ESP32-S2 e o S3 têm 4. O contador tem 16 bits e inclui um filtro de glitches que remove ruídos curtos. Esse filtro pode ser ajustado para no máximo cerca de 12,8 microssegundos, então ele só engole picos muito curtos, não todo o repique dos contatos. A contagem em quatro bordas compensa sozinha o resto do repique: os repiques de ida e volta se anulam como +1 e −1. O código abaixo usa o driver do ESP-IDF que vem com a versão 3.x do core Arduino ESP32.

C++ (Arduino IDE)
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}

Os dois canais são configurados como imagens espelhadas um do outro: o canal A conta as bordas do CLK e inverte a direção conforme o nível do DT, enquanto o canal B conta as bordas do DT em relação ao CLK. Com isso, o contador muda 4 a cada clique. Ao chegar ao limite de ±10000, ele volta a zero; se você precisa de uma faixa maior, pode acumular os estouros com a opção flags.accum_count e com pontos de observação (watch points). O ESP32-C3 não tem unidade PCNT; nessa placa, use o código Arduino baseado em interrupções. Se você está na versão 2.x do core Arduino ESP32 ou quer um código mais curto, a biblioteca ESP32Encoder usa o mesmo hardware e resolve com as funções attachFullQuad() e getCount(). Se você usa MicroPython no ESP32, a classe machine.Encoder das versões recentes (por exemplo, 1.27) também usa o PCNT por trás. Se você está começando com o ESP32, leia primeiro O que é o ESP32.

Encoder rotativo com o Raspberry Pi Pico e MicroPython

Todo pino GPIO do Pico pode disparar uma interrupção, então ler um encoder em MicroPython com Pin.irq() é fácil. O código abaixo usa exatamente a mesma tabela de estados do exemplo do Arduino e transforma o encoder em um dimmer de LED: cada clique muda o brilho em 5 por cento e apertar o eixo liga e desliga o LED. Ligue o CLK no GP16, o DT no GP17, o SW no GP18 e um LED, por meio de um resistor de 220 Ω, no GP15. Para configurar o MicroPython, veja o artigo O que é o 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)

Verificamos os limites do valor (0–20 cliques) dentro da função de interrupção, então o laço principal só lê a variável e não há condição de corrida em que os dois lados escrevem ao mesmo tempo. Você encontra como o brilho do LED é controlado no artigo O que é PWM. Se preferir uma solução pronta, o módulo rotary_irq_rp2 do projeto micropython-rotary já vem com limites, volta ao início (wrap-around) e inversão de direção. Para encoders de motor muito rápidos, o bloco PIO do Pico consegue contar sem carregar o processador. Se você está começando no MicroPython, o artigo fundamentos do MicroPython é um bom ponto de partida.

Tipos de encoder rotativo: mecânico, óptico e magnético

TipoComo funciona?Resolução típicaOnde é usado?
Mecânico (KY-040, EC11)Disco de contato e duas chaves12–30 cliques por voltaBotões de menu, controles de volume, painéis frontais de aparelhos
ÓpticoDisco com fendas, LED e fototransistorDe centenas a milhares de pulsos por voltaMáquinas CNC, juntas de robôs, motores industriais
Magnético (efeito Hall)Disco magnético e sensores HallAlguns pulsos por volta do eixo do motor (por exemplo, 7 ou 11)Motores N20 e motores CC com redução e encoder
AbsolutoGera um código único para cada posiçãoPor exemplo, 12 bits (4096 posições) no AS5600Braços robóticos, medição de ângulo; sabe a posição ao ligar

O código deste artigo também funciona com outros encoders incrementais que têm saídas A/B. Por exemplo, em um motor CC com redução e encoder, um encoder Hall que dá 11 pulsos por volta do eixo do motor, a contagem em quatro bordas e uma relação de redução de 1:30 resultam em 11 × 4 × 30 = 1320 contagens por volta do eixo de saída. Quando a taxa de pulsos chega aos milhares por segundo, o código baseado em interrupções toma uma parte considerável do tempo do processador, enquanto a unidade PCNT do ESP32 faz essa contagem sem esforço. Encoders ópticos industriais costumam trabalhar com 5–24 V e têm saídas em coletor aberto ou diferenciais; sempre confira o tipo de saída e a tensão no datasheet antes de ligar um.

Ideias de projetos com encoder rotativo

  • Botão de volume digital: ajuste o volume da música tocando em um DFPlayer Mini com o encoder e aperte o eixo para pular para a próxima faixa.
  • Luminária com seletor de cor: gire a matiz de um anel de LEDs WS2812B com o encoder e aperte para passar ao modo de brilho.
  • Posicionamento preciso: faça cada clique mover um motor de passo um número definido de passos; monte um slider de câmera ou uma base giratória.
  • Timer de cozinha: ajuste o tempo com o encoder, aperte para iniciar a contagem regressiva e toque um buzzer quando o tempo acabar.
  • Controle de jogo: use o encoder como a raquete do Pong; nos projetos de jogos com Arduino ele dá um controle mais fino que um joystick.
  • Ajuste de relógio e alarme: ajuste a hora e o alarme de um módulo de relógio RTC com um único botão.
  • Painel de instrumento de teste: mude uma frequência de PWM, um valor de gerador de sinais ou a temperatura alvo de uma estação de solda em passos finos.

Erros comuns e como resolvê-los

ProblemaCausa provávelSolução
Conta 2 ou 4 por cliqueCada borda é contada separadamente ou os contatos repicamDivida a contagem de bordas por 4, use uma tabela de estados, adicione capacitores de 10–100 nF em CLK e DT
Conta só a cada dois cliquesEncoder de 30 cliques / 15 pulsos; cada clique é meio cicloDivida o valor da biblioteca por 2 em vez de 4; no código com tabela, troque if (ab == 3) por if (ab == 3 || ab == 0)
A direção está invertidaCLK e DT trocados ou módulo diferenteTroque os números dos pinos CLK e DT no código
Passos se perdem ao girar rápidoEstá usando polling, há um delay no loopUse interrupções ou o PCNT do ESP32, mantenha a ISR curta
O botão se aperta sozinhoPino SW flutuando, sem pull-upUse pinMode(SW, INPUT_PULLUP)
O contador pula aleatoriamenteSem GND comum, fios longos ou soltosCompartilhe o GND, encurte os fios, confira os contatos da protoboard
fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit' (erro de compilação)Core Arduino ESP32 2.x (primeiro erro) ou um ESP32-C3, que não tem unidade PCNT (segundo erro)Atualize o core para 3.x no Gerenciador de Placas ou use a biblioteca ESP32Encoder; no C3, use o código com interrupções
O LCD só mostra blocosContraste mal ajustado ou endereço I2C erradoGire o trimpot na parte de trás, tente o endereço 0x3F em vez de 0x27
Se você quer testar o código antes de montar o circuito, o simulador Wokwi tem uma peça KY-040 pronta: depois de clicar no encoder, você pode girá-lo clique a clique com as setas do teclado e apertar o botão com a barra de espaço. Para a configuração, veja o guia simulação no Wokwi.

O encoder rotativo é um dos jeitos mais baratos de dar a um projeto uma interface de usuário profissional: um único botão e um display dão conta de dezenas de configurações. Comece pelo código com polling para entender a lógica, depois passe para a versão com interrupções ou com PCNT e adapte o exemplo de menu ao seu projeto. No app BlueGrays você encontra projetos de controle de servo com potenciômetro, LCD I2C, botão de pressão e contador com display de 7 segmentos, com esquemas do circuito e código passo a passo, e pode melhorá-los facilmente trocando o potenciômetro ou o botão por um encoder.

Perguntas frequentes

Qual é a diferença entre um encoder rotativo e um potenciômetro?

Um potenciômetro fornece uma tensão analógica que depende da posição do eixo, sua rotação é limitada a cerca de 270–300 graus e você o lê com analogRead. Um encoder rotativo gira sem fim e gera pulsos digitais a cada clique; ele informa a direção e a quantidade de movimento, não a posição. O encoder é melhor para navegar em menus e para ajustes em passos; o potenciômetro, para ajustes simples baseados em posição.

Por que meu encoder rotativo conta dois passos por clique ou pula passos?

Contar demais costuma ser culpa do repique dos contatos ou de contar cada borda separadamente; dividir as 4 bordas de cada clique por 4 e usar uma tabela de estados resolve. Passos pulados acontecem quando os pulsos chegam enquanto o loop está ocupado. Ligue CLK e DT em pinos de interrupção e leia-os com interrupções, adicione capacitores de 10–100 nF se precisar, e o problema some.

O encoder rotativo KY-040 funciona em 3,3 V?

Sim. O KY-040 é formado só por chaves mecânicas e resistores de pull-up, então funciona perfeitamente em 3,3 V também. No ESP32 e no Raspberry Pi Pico, ligue sempre o pino + no 3V3; se você alimentá-lo com 5 V, os resistores de pull-up puxam CLK e DT para 5 V e o limite das entradas de 3,3 V é ultrapassado.

Quantos pulsos por volta o KY-040 dá?

Os módulos KY-040 comuns têm 20 cliques por volta, e as linhas A e B completam um ciclo inteiro a cada clique. Cada ciclo tem quatro bordas, então você conta 80 bordas por volta, e cada clique vale 18 graus. Alguns encoders têm 30 cliques e 15 pulsos; neles, cada clique corresponde a meio ciclo.

Dá para ler um encoder rotativo sem interrupções?

Sim, dá para ler por polling, verificando o pino CLK o tempo todo dentro do loop, e isso costuma bastar para um botão de menu girado devagar. Mas se o loop tiver delay, atualizações de display ou cálculos longos, os cliques dados rápido vão se perder. Para resultados confiáveis, recomenda-se usar interrupções e, no ESP32, o contador de hardware PCNT.

Posso ligar mais de um encoder rotativo em um Arduino?

Sim. Como no Uno só D2 e D3 têm interrupção de hardware, você pode ligar o primeiro pino de cada encoder (o que é passado primeiro ao construtor) em um pino de interrupção e o outro em um pino normal usando a biblioteca Encoder, ou usar interrupções por mudança de pino (pin change). O Mega tem seis pinos de interrupção, o ESP32 clássico consegue contar 8 encoders no hardware com suas 8 unidades PCNT, e no Pico todo GPIO pode disparar uma interrupção.

Ver todos os projetos
Ligar e desligar um LED com botão
#10Iniciante

Ligar e desligar um LED com botão

Pressione o botão uma vez para acender a luz e outra vez para apagar, como o interruptor do seu quarto. O Arduino detecta cada toque e inverte o estado do LED, aguardando um breve instante para que as

4.8(100)
167
Controle de LED com botão no ESP32 (alternância)
#105Iniciante

Controle de LED com botão no ESP32 (alternância)

Cada vez que o botão é pressionado, o estado do LED muda: se estiver aceso, apaga; se estiver apagado, acende. É o padrão clássico de alternância. Com debounce, eliminamos o ruído mecânico dos contatos do botão para garantir uma única mudança a cada pressionamento.

4.6(39)
57
Raspberry Pi Pico: debounce de botão com IRQ ou consulta por software
#159Intermediário
Premium

Raspberry Pi Pico: debounce de botão com IRQ ou consulta por software

Você pressiona o botão uma vez e o código conta 3-4? Isso é oscilação dos contatos — os contatos metálicos oscilam por microssegundos! Compare 2 métodos de contagem lado a lado no Monitor Serial: consulta sem filtro (detecta as oscilações e conta a mais) e IRQ + debounce por software (contagem correta). Aprenda a física da oscilação dos contatos, debounce por software (ticks_diff() > 50ms), registro de interrupção com Pin.irq(), disparo IRQ_FALLING, rotinas ISR, global e…

0.0(0)
12
Uso de servo motor com potenciômetro
#36Iniciante
Premium

Uso de servo motor com potenciômetro

Conforme você gira o botão do potenciômetro, o braço do servo motor gira junto: uma extremidade do botão corresponde a zero grau, e a outra, a meia volta. O Arduino lê continuamente a posição do botão

4.6(37)
52
Brilho do LED com potenciômetro no ESP32 (analógico → PWM)
#115Intermediário
Premium

Brilho do LED com potenciômetro no ESP32 (analógico → PWM)

Neste projeto, controlamos o brilho de um LED de forma analógica com um potenciômetro. O pino ADC do ESP32 lê a tensão de 0-3,3V do potenciômetro e converte esse valor em um sinal PWM (modulação por largura de pulso), enviado ao LED. Assim, ao girar o potenciômetro, o brilho do LED aumenta e diminui gradualmente. Com Arduino Core 3.0+, você pode usar analogWrite() diretamente; no ESP32-S2/S3, a resolução do ADC é fixada em 12 bits.

4.5(19)
32
Contador com display de 7 segmentos e botões
#43Avançado
Premium

Contador com display de 7 segmentos e botões

Um contador simples com três botões: ao pressionar um deles, o algarismo no display aumenta em um; com outro, diminui em um; e o terceiro zera a contagem. O display de 7 segmentos de um dígito conta d

4.6(35)
47
Uso do display LCD com I2C
#32Iniciante
Premium

Uso do display LCD com I2C

Dê ao seu Arduino um display próprio e faça-o escrever sua primeira mensagem. O display LCD de duas linhas é conectado com poucos fios; primeiro aparece uma saudação na linha de cima, depois uma segun

4.8(53)
70
Jogo de bola e raquetes - SSD1306
#86Intermediário
Premium

Jogo de bola e raquetes - SSD1306

Uma bola, duas raquetes e você: jogue tênis de mesa, um dos primeiros videogames, em um pequeno display OLED. Dois botões movem sua raquete à direita para cima e para baixo, enquanto o computador cont

4.5(47)
65