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ística | Valor no KY-040 |
|---|---|
| Tipo | Mecânico, incremental, saídas A/B |
| Detentes | 20 cliques por volta (30 em alguns encoders) |
| Pulsos | 20 ciclos A/B completos por volta, ou seja, 80 bordas |
| Resolução angular | 18 graus por clique |
| Pinos | CLK (A), DT (B), SW (botão), + (alimentação), GND |
| Alimentação | 3,3–5 V (só alimenta os resistores de pull-up) |
| Resistores embutidos | Pull-ups de 10 kΩ em CLK e DT; o resistor do SW não vem em todos os módulos |
| Botão | Normalmente aberto; apertar o eixo liga SW ao GND (ativo em LOW) |
| Rotação | Ilimitada, 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.

| Característica | Potenciômetro | Encoder rotativo (KY-040) |
|---|---|---|
| Rotação | Cerca de 270–300 graus, limitada | Ilimitada, 360 graus contínuos |
| Saída | Tensão analógica (0–5 V) | Pulsos digitais (A e B) |
| Leitura | analogRead (ADC) | digitalRead ou contagem com interrupções |
| Posição | Absoluta; a posição do eixo dá o valor mesmo depois de uma queda de energia | Relativa; a posição é desconhecida ao ligar |
| Sensação de clique | Nenhuma, gira suavemente | Sim, normalmente 20 cliques por volta |
| Botão | Não | Sim (apertando o eixo) |
| Problema típico | Chiado e valores que variam com o tempo | Repique dos contatos (resolvido no software) |
| Uso típico | Volume, ângulo de servo, ajuste analógico | Navegaçã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:
| Passo | Sentido horário (CLK, DT) | Sentido anti-horário (CLK, DT) |
|---|---|---|
| Parado em um detente | 1, 1 | 1, 1 |
| 1ª transição | 0, 1 | 1, 0 |
| 2ª transição | 0, 0 | 0, 0 |
| 3ª transição | 1, 0 | 0, 1 |
| 4ª transição (próximo clique) | 1, 1 | 1, 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-040 | Função | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Fase A | D2 | GPIO 25 | GP16 |
| DT | Fase B | D3 | GPIO 26 | GP17 |
| SW | Botão (LOW quando pressionado) | D4 | GPIO 27 | GP18 |
| + | Alimentação | 5V | 3V3 | 3V3(OUT) |
| GND | Terra | GND | GND | GND |
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
- Ligue o pino CLK do KY-040 no D2, o DT no D3 e o SW no D4.
- Ligue o pino + no 5V e o GND no GND do Arduino.
- Cole o código abaixo no Arduino IDE e carregue-o na placa.
- Abra o Monitor Serial e ajuste a velocidade para 115200 baud.
- Gire o botão alguns cliques nos dois sentidos e depois aperte o eixo para zerar o contador.
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.
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ódigo | O 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 |
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.
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.
Navegação de menu com encoder rotativo em um LCD

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 Ω.
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().
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 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.
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.
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
| Tipo | Como funciona? | Resolução típica | Onde é usado? |
|---|---|---|---|
| Mecânico (KY-040, EC11) | Disco de contato e duas chaves | 12–30 cliques por volta | Botões de menu, controles de volume, painéis frontais de aparelhos |
| Óptico | Disco com fendas, LED e fototransistor | De centenas a milhares de pulsos por volta | Máquinas CNC, juntas de robôs, motores industriais |
| Magnético (efeito Hall) | Disco magnético e sensores Hall | Alguns pulsos por volta do eixo do motor (por exemplo, 7 ou 11) | Motores N20 e motores CC com redução e encoder |
| Absoluto | Gera um código único para cada posição | Por exemplo, 12 bits (4096 posições) no AS5600 | Braç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
| Problema | Causa provável | Solução |
|---|---|---|
| Conta 2 ou 4 por clique | Cada borda é contada separadamente ou os contatos repicam | Divida 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 cliques | Encoder de 30 cliques / 15 pulsos; cada clique é meio ciclo | Divida 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á invertida | CLK e DT trocados ou módulo diferente | Troque os números dos pinos CLK e DT no código |
| Passos se perdem ao girar rápido | Está usando polling, há um delay no loop | Use interrupções ou o PCNT do ESP32, mantenha a ISR curta |
| O botão se aperta sozinho | Pino SW flutuando, sem pull-up | Use pinMode(SW, INPUT_PULLUP) |
| O contador pula aleatoriamente | Sem GND comum, fios longos ou soltos | Compartilhe 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 blocos | Contraste mal ajustado ou endereço I2C errado | Gire o trimpot na parte de trás, tente o endereço 0x3F em vez de 0x27 |
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.
Projetos para montar depois desta leitura
Ver todos os projetos
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

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.

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…

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

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.

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

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

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
Mais artigos
App BlueGrays
Monte este circuito passo a passo pelo celular
Esquema do circuito, código e lista de componentes em uma só tela. Mais de 200 projetos com Arduino, ESP32 e Pico prontos para montar.
Escaneie com seu celular
Aponte a câmera para o código para abrir a loja de aplicativos do seu celular.
ou abrir a loja

