Encodeur rotatif Arduino : KY-040, interruptions et menu LCD
Encodeur rotatif avec Arduino : câblage du KY-040, signaux A/B en quadrature, interruptions sans perte de pas, anti-rebond, menu LCD, ESP32 PCNT et Pico.
Un encodeur rotatif est un bouton de réglage numérique pour les projets Arduino qui tourne sans fin, compte un pas à chaque cran et fonctionne comme un bouton-poussoir quand tu appuies sur son axe. Branché sur un Arduino, un encodeur rotatif se comporte comme le bouton de menu d'une imprimante 3D, la molette de réglage d'un oscilloscope ou le bouton de volume d'une voiture : il sait de combien de crans tu as tourné et dans quel sens, et il ne bute jamais sur une fin de course. Le modèle que tu trouveras le plus souvent dans les kits est le module KY-040 à cinq broches.
Dans ce guide, tu vas apprendre en quoi le KY-040 diffère d'un potentiomètre, comment le sens de rotation se déduit des phases A et B (quadrature), comment filtrer les rebonds des contacts par le matériel et par le logiciel, et comment compter chaque cran sans en perdre un seul grâce aux interruptions. Ensuite, nous mettrons en place pas à pas le raccourci avec la bibliothèque Encoder, la navigation dans un menu sur LCD et OLED, le compteur matériel PCNT de l'ESP32 et le code MicroPython pour la Raspberry Pi Pico.
Qu'est-ce qu'un encodeur rotatif et comment fonctionne-t-il ?
À l'intérieur d'un encodeur mécanique comme le KY-040 se trouvent un disque de contact denté qui tourne avec l'axe et deux petits interrupteurs qui frottent dessus. Quand l'axe tourne, les deux interrupteurs s'ouvrent et se ferment l'un après l'autre, et des impulsions carrées apparaissent sur les broches CLK (A) et DT (B). Les deux signaux sont décalés d'un quart de période, soit 90 degrés ; on parle de signal en quadrature. Le microcontrôleur trouve le sens en regardant quel signal change en premier, et l'amplitude de la rotation en comptant les changements. L'encodeur ne mémorise pas sa position, il signale seulement le mouvement. C'est pourquoi le KY-040 est un encodeur incrémental : à la mise sous tension la position vaut zéro, et tout le comptage est assuré par ton code.
| Caractéristique | Valeur pour le KY-040 |
|---|---|
| Type | Mécanique, incrémental, sorties A/B |
| Crans | 20 crans par tour (30 sur certains encodeurs) |
| Impulsions | 20 cycles A/B complets par tour, soit 80 fronts |
| Résolution angulaire | 18 degrés par cran |
| Broches | CLK (A), DT (B), SW (bouton), + (alimentation), GND |
| Alimentation | 3,3–5 V (alimente seulement les résistances de pull-up) |
| Résistances intégrées | Pull-up de 10 kΩ sur CLK et DT ; la résistance de SW n'est pas montée sur tous les modules |
| Bouton | Normalement ouvert ; un appui sur l'axe relie SW à GND (actif à l'état LOW) |
| Rotation | Illimitée, sans butée |
Encodeur rotatif ou potentiomètre : quelle différence ?
Au premier coup d'œil, un encodeur ressemble beaucoup à un potentiomètre : tous deux ont un axe rotatif et servent à régler une valeur. Mais leur fonctionnement est totalement différent. Un potentiomètre est un diviseur de tension ; il fournit une tension analogique entre 0 et 5 V selon la position de l'axe, que tu lis avec analogRead. Un encodeur produit des impulsions et non une tension ; il t'indique le changement, pas la position.

| Caractéristique | Potentiomètre | Encodeur rotatif (KY-040) |
|---|---|---|
| Rotation | Environ 270–300 degrés, limitée | Illimitée, 360 degrés en continu |
| Sortie | Tension analogique (0–5 V) | Impulsions numériques (A et B) |
| Lecture | analogRead (CAN) | digitalRead ou comptage par interruption |
| Position | Absolue ; la position de l'axe donne la valeur même après une coupure de courant | Relative ; la position est inconnue à la mise sous tension |
| Sensation de cran | Aucune, rotation douce | Oui, en général 20 crans par tour |
| Bouton | Non | Oui (appui sur l'axe) |
| Problème typique | Grésillements et valeurs instables avec le temps | Rebonds des contacts (corrigés par logiciel) |
| Usage typique | Volume, angle de servo, réglage analogique | Navigation dans les menus, réglages numériques par pas, façades d'appareils |
En résumé, si la valeur doit correspondre exactement à la position physique de l'axe (orienter à la main le bras d'un servo ou un simple réglage de volume), le potentiomètre est plus simple. Si tu veux parcourir un menu, augmenter et diminuer un nombre par pas précis, modifier plusieurs réglages avec un seul bouton ou changer aussi la valeur depuis le logiciel, l'encodeur est bien plus souple. Par exemple, si une valeur affichée à 50 passe à 80 depuis ton téléphone, en tournant l'encodeur tu repars simplement de 80 ; avec un potentiomètre, la position du bouton et la valeur à l'écran ne correspondent plus.
Phases A et B : le principe de la lecture en quadrature
Quand le KY-040 est au repos sur un cran, les deux interrupteurs sont ouverts et, grâce aux résistances de pull-up, CLK et DT lisent tous deux HIGH (1). Quand tu tournes le bouton d'un cran, les deux lignes passent à LOW l'une après l'autre, puis reviennent à HIGH l'une après l'autre. Dans le sens horaire, CLK descend en premier ; dans le sens antihoraire, c'est DT. Cette séquence à quatre états est un code Gray : un seul bit change à chaque étape. Le tableau montre ce que lisent les deux broches pendant un cran de rotation :
| Étape | Sens horaire (CLK, DT) | Sens antihoraire (CLK, DT) |
|---|---|---|
| Au repos sur un cran | 1, 1 | 1, 1 |
| 1re transition | 0, 1 | 1, 0 |
| 2e transition | 0, 0 | 0, 0 |
| 3e transition | 1, 0 | 0, 1 |
| 4e transition (cran suivant) | 1, 1 | 1, 1 |
Le tableau donne deux règles simples. Première règle : si DT est encore à HIGH quand CLK descend, la rotation est horaire ; si DT est déjà à LOW, elle est antihoraire. Deuxième règle : un cran comporte quatre transitions, donc 20 crans par tour font 80 transitions. Un code simple n'utilise que la première règle et compte une fois par cran. Un code robuste suit les quatre transitions et peut ainsi écarter les mouvements qui rebondissent ou restent à mi-chemin. Le sens considéré comme horaire peut varier selon le module et le câblage ; si ton compteur avance à l'envers, il suffit d'inverser les broches CLK et DT dans le code.
Brochage du KY-040 et câblage
| Broche du KY-040 | Rôle | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|---|
| CLK | Phase A | D2 | GPIO 25 | GP16 |
| DT | Phase B | D3 | GPIO 26 | GP17 |
| SW | Bouton (LOW à l'appui) | D4 | GPIO 27 | GP18 |
| + | Alimentation | 5V | 3V3 | 3V3(OUT) |
| GND | Masse | GND | GND | GND |
Sur l'Arduino Uno et le Nano, les interruptions matérielles ne sont disponibles que sur les broches D2 et D3 ; le mieux est donc d'y brancher CLK et DT. Sur le Mega, les broches d'interruption sont 2, 3, 18, 19, 20 et 21 ; sur le Leonardo, 0, 1, 2, 3 et 7. Sur l'ESP32 et la Pico, presque toutes les GPIO peuvent déclencher une interruption. Tu peux consulter le brochage de ta carte dans l'outil Pinout. Utilise INPUT_PULLUP pour la broche SW dans le code : certains modules n'ont pas de résistance sur cette ligne, et une broche en l'air se lit comme si on appuyait au hasard. Sur l'ESP32, les GPIO 34 à 39 sont uniquement des entrées sans pull-up interne ; ne les choisis pas pour SW.
Premier essai pas à pas : lecture par scrutation (polling)
- Relie la broche CLK du KY-040 à D2, DT à D3 et SW à D4.
- Relie la broche + au 5V et GND au GND de l'Arduino.
- Colle le code ci-dessous dans l'Arduino IDE et téléverse-le sur la carte.
- Ouvre le Moniteur série et règle la vitesse sur 115200 bauds.
- Tourne le bouton de quelques crans dans les deux sens, puis appuie sur l'axe pour remettre le compteur à zéro.
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}À chaque passage dans loop, le code lit CLK et ne repère que l'instant où CLK passe de HIGH à LOW. Si DT est encore à HIGH à ce moment-là, le bouton a tourné dans le sens horaire ; s'il est à LOW, dans le sens antihoraire. C'est la méthode la plus facile à comprendre, mais elle a deux points faibles. D'abord, les impulsions qui arrivent pendant que loop est occupée à autre chose (mise à jour d'un écran, delay, lecture de capteurs) sont perdues, et le compteur prend du retard si tu tournes vite. Ensuite, les contacts mécaniques rebondissent : CLK peut faire plusieurs fois LOW-HIGH en descendant une seule fois, et le code peut compter un cran comme deux ou trois. Les deux sections suivantes règlent ces deux problèmes.
Rebonds des contacts (anti-rebond) : solutions matérielles et logicielles
Les ressorts métalliques de l'encodeur rebondissent plusieurs fois en touchant le contact avant de se stabiliser. Les fiches techniques des fabricants indiquent généralement quelques millisecondes (par exemple 2 ms au maximum). Avec les boutons-poussoirs, tu règles le rebond en disant "attendre 20 à 50 ms après le dernier changement", mais cela ne marche pas pour un encodeur : quand on le tourne vite, il n'y a que quelques millisecondes entre deux fronts, et une longue attente avale aussi de vrais pas. On utilise plutôt ces méthodes :
- Filtre matériel : Place un condensateur de 10 à 100 nF entre CLK et GND et un autre entre DT et GND. Avec les pull-up de 10 kΩ du module, ils forment un filtre RC ; la constante de temps va de 10 kΩ × 10 nF = 0,1 ms à 10 kΩ × 100 nF = 1 ms et absorbe les rebonds courts. Tu peux la calculer avec l'outil constante de temps RC et décoder un condensateur marqué 103 ou 104 avec l'outil code des condensateurs.
- Table d'états : À chaque front des deux lignes, on compare l'ancien et le nouvel état AB. Un rebond revient à faire des allers-retours entre les deux mêmes états, ce qui produit tour à tour +1 et −1, qui s'annulent. Les transitions où les deux bits changent en même temps sont jugées invalides et ignorées.
- Décider seulement sur un cran : Les quarts de pas sont cumulés et le compteur n'est incrémenté ou décrémenté que lorsque l'encodeur s'arrête sur un cran (les deux lignes à HIGH). Ainsi, un contact qui vibre sur un cran ne fait pas sauter le curseur du menu.
- Verrouillage temporel pour le bouton : La broche SW se comporte comme un bouton-poussoir ordinaire ; un verrou de 30 ms avec millis() suffit.
Lecture par interruption pour ne perdre aucun pas
Une interruption oblige le processeur à abandonner ce qu'il fait dès qu'une broche change pour exécuter une courte fonction (ISR). Une fois les lignes de l'encodeur reliées à des interruptions, aucun front n'est perdu, même si loop contient un delay de 50 ms ou si l'écran se met à jour lentement. Le code ci-dessous s'exécute sur les deux fronts de CLK et DT, utilise la table d'états décrite plus haut et ne compte que lorsque le bouton s'arrête sur un cran. Pour les bases, consulte aussi l'article interruptions 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}| Partie du code | À quoi sert-elle ? |
|---|---|
| Table QEM[16] | Transforme l'ancien et le nouvel état AB en +1, −1 ou 0 ; les transitions invalides où les deux lignes changent à la fois comptent pour 0 |
| attachInterrupt(..., CHANGE) | Exécute readEncoder() sur les fronts montants et descendants de CLK et DT |
| quarter | Cumule les quarts de pas entre deux crans ; les paires +1 et −1 dues aux rebonds s'annulent |
| if (ab == 3) | La décision n'est prise que lorsque l'encodeur est sur un cran (les deux lignes à HIGH) ; un cumul de +2 ou plus compte pour un cran |
| volatile long position | La variable partagée par l'ISR et loop ; volatile empêche le compilateur d'utiliser une copie périmée |
| noInterrupts() / interrupts() | Empêche l'ISR de s'intercaler pendant la copie du long de 4 octets |
| buttonPressed() | Anti-rebond de SW avec un verrou de 30 ms ; renvoie true une seule fois, au moment de l'appui |
Le raccourci : la bibliothèque Encoder
Si tu ne veux pas écrire ta propre ISR, la bibliothèque Encoder de Paul Stoffregen fait le même travail avec un code optimisé. Ouvre le gestionnaire de bibliothèques de l'Arduino IDE, cherche "Encoder" et installe la bibliothèque de Paul Stoffregen ; pour les étapes d'installation, jette un œil au guide ajouter une bibliothèque Arduino. La bibliothèque offre les meilleures performances quand les deux broches sont des broches d'interruption, de bonnes performances quand seule la première broche passée au constructeur l'est, et des performances faibles quand aucune ne l'est. Comme elle compte chaque front, la valeur change de 4 par cran ; divise par 4 pour obtenir le nombre de crans.
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() renvoie le nombre total de fronts jusqu'ici, write(0) remet le compteur à zéro et readAndReset() lit et remet à zéro en une seule fois. Nous passons d'abord DT puis CLK au constructeur à cause du signe du sens de rotation : dans cet ordre, le sens horaire compte en positif. Si c'est inversé chez toi, permute 2 et 3. La bibliothèque active aussi d'elle-même les résistances de pull-up internes des broches.
Navigation dans un menu avec un encodeur rotatif sur LCD

C'est dans les menus que les encodeurs sont le plus utilisés : tu tournes le bouton pour passer d'une ligne à l'autre, tu appuies pour mettre le réglage sélectionné en mode édition, tu tournes pour changer la valeur et tu appuies encore pour revenir au menu. L'exemple ci-dessous règle les canaux rouge, vert et bleu d'une LED RGB entre 0 et 255 et affiche les valeurs sur un écran LCD I2C. Relie le LCD à A4 (SDA) et A5 (SCL), et les pattes de couleur d'une LED RGB à cathode commune à D9, D10 et D11 via des résistances 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}Un signe > apparaît au début de la ligne sélectionnée, et un signe * en mode édition. Le code de l'encodeur est le même que dans l'exemple précédent ; la seule différence est que l'ISR cumule le nombre de crans non traités (steps) au lieu d'une position, et que loop le lit puis le remet à zéro. Ainsi, aucun cran n'est perdu même si loop reste occupée un moment : les crans cumulés sont traités ensemble au passage suivant. En mode édition, chaque cran modifie la valeur de 5 ; pour un réglage plus fin, tu peux réduire le pas à 1 ou ajouter une accélération qui augmente le pas quand on tourne vite.
Pour afficher le même menu sur un écran OLED, il suffit de changer la fonction drawMenu(). La version ci-dessous dessine la ligne sélectionnée en couleurs inversées sur une barre blanche. Inclus les bibliothèques Adafruit SSD1306 et Adafruit GFX à la place de celle du LCD, supprime l'objet lcd et appelle display.begin() au lieu de lcd.init() dans 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}Encodeur rotatif avec l'ESP32 : le compteur matériel PCNT
L'ESP32 possède un bloc matériel appelé PCNT (Pulse Counter) qui compte les impulsions sans occuper le processeur. Chaque unité a deux canaux et peut donc décoder directement le signal A/B ; l'ESP32 classique compte 8 unités PCNT, les ESP32-S2 et S3 en ont 4. Le compteur fait 16 bits et intègre un filtre anti-parasites (glitch filter) qui élimine les perturbations courtes. Ce filtre se règle au maximum à environ 12,8 microsecondes : il n'avale donc que des pointes très brèves, pas l'ensemble des rebonds. Le comptage sur quatre fronts compense de lui-même le reste : les allers-retours dus aux rebonds s'annulent en +1 et −1. Le code ci-dessous utilise le pilote ESP-IDF fourni avec la version 3.x du cœur 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}Les deux canaux sont configurés en miroir : le canal A compte les fronts de CLK et inverse le sens selon le niveau de DT, le canal B compte les fronts de DT par rapport à CLK. Résultat : le compteur varie de 4 par cran. Quand il atteint la limite de ±10000, il revient à zéro ; s'il te faut une plage plus large, tu peux cumuler les débordements avec l'option flags.accum_count et des points de surveillance (watch points). L'ESP32-C3 n'a pas d'unité PCNT ; sur cette carte, utilise le code Arduino par interruption. Si tu es sur la version 2.x du cœur Arduino ESP32 ou si tu veux un code plus court, la bibliothèque ESP32Encoder utilise le même matériel et fait le travail avec ses fonctions attachFullQuad() et getCount(). Si tu programmes l'ESP32 en MicroPython, la classe machine.Encoder des versions récentes (par exemple 1.27) s'appuie aussi sur le PCNT. Si tu débutes avec l'ESP32, lis d'abord Qu'est-ce que l'ESP32.
Encodeur rotatif avec la Raspberry Pi Pico et MicroPython
Toutes les broches GPIO de la Pico peuvent déclencher une interruption, il est donc facile de lire un encodeur en MicroPython avec Pin.irq(). Le code ci-dessous reprend exactement la même table d'états que l'exemple Arduino et transforme l'encodeur en variateur de LED : chaque cran modifie la luminosité de 5 pour cent et un appui sur l'axe allume ou éteint la LED. Relie CLK à GP16, DT à GP17, SW à GP18 et une LED avec une résistance de 220 Ω à GP15. Pour installer MicroPython, consulte l'article Qu'est-ce que la Raspberry Pi Pico.
1from machine import Pin, PWM
2import time
3
4clk = Pin(16, Pin.IN, Pin.PULL_UP) # A fazı / phase A
5dt = Pin(17, Pin.IN, Pin.PULL_UP) # B fazı / phase B
6sw = Pin(18, Pin.IN, Pin.PULL_UP) # buton, basınca 0 / button, 0 when pressed
7led = PWM(Pin(15)) # LED + 220 ohm
8led.freq(1000)
9
10# İndeks: (eski AB << 2) | yeni AB / Index: (old AB << 2) | new AB
11QEM = (0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0)
12last_ab = (clk.value() << 1) | dt.value()
13quarter = 0
14position = 10 # 0-20 tık, başlangıç %50 / 0-20 detents, start at 50 %
15
16def on_edge(pin):
17 global last_ab, quarter, position
18 ab = (clk.value() << 1) | dt.value()
19 quarter += QEM[(last_ab << 2) | ab]
20 last_ab = ab
21 if ab == 3: # tıka oturdu / settled in a detent
22 if quarter > 1 and position < 20:
23 position += 1
24 elif quarter < -1 and position > 0:
25 position -= 1
26 quarter = 0
27
28clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
29dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
30
31on = True
32shown = -1
33while True:
34 if sw.value() == 0: # mile basıldı / shaft pressed
35 time.sleep_ms(20) # sıçramayı bekle / wait out the bounce
36 if sw.value() == 0:
37 on = not on
38 shown = -1 # yeniden yaz / force an update
39 while sw.value() == 0: # bırakılmasını bekle / wait for release
40 time.sleep_ms(10)
41 if position != shown:
42 shown = position
43 led.duty_u16(shown * 65535 // 20 if on else 0)
44 print("Parlaklik / brightness: %d%%" % (shown * 5), "acik / on" if on else "kapali / off")
45 time.sleep_ms(10)Nous vérifions les limites de la valeur (0 à 20 crans) dans la fonction d'interruption ; ainsi la boucle principale ne fait que lire la variable et il n'y a pas de situation de concurrence où les deux côtés écrivent en même temps. Tu trouveras comment la luminosité de la LED est réglée dans l'article Qu'est-ce que le PWM. Si tu préfères une solution toute faite, le module rotary_irq_rp2 du projet micropython-rotary propose limites, rebouclage (wrap) et inversion du sens. Pour des encodeurs de moteur très rapides, le bloc PIO de la Pico peut compter sans solliciter le processeur. Si tu débutes en MicroPython, l'article les bases de MicroPython est un bon point de départ.
Types d'encodeurs rotatifs : mécanique, optique et magnétique
| Type | Comment fonctionne-t-il ? | Résolution typique | Où l'utilise-t-on ? |
|---|---|---|---|
| Mécanique (KY-040, EC11) | Disque de contact et deux interrupteurs | 12 à 30 crans par tour | Boutons de menu, réglage du volume, façades d'appareils |
| Optique | Disque à fentes, LED et phototransistor | De centaines à milliers d'impulsions par tour | Machines CNC, articulations de robots, moteurs industriels |
| Magnétique (effet Hall) | Disque aimanté et capteurs à effet Hall | Quelques impulsions par tour d'arbre moteur (par ex. 7 ou 11) | Moteurs N20 et motoréducteurs à courant continu avec encodeur |
| Absolu | Produit un code unique pour chaque position | Par exemple 12 bits (4096 positions) pour l'AS5600 | Bras robotisés, mesure d'angle ; connaît sa position à la mise sous tension |
Le code de cet article fonctionne aussi avec d'autres encodeurs incrémentaux à sorties A/B. Par exemple, sur un motoréducteur à courant continu équipé d'un encodeur, un encodeur à effet Hall donnant 11 impulsions par tour d'arbre moteur, le comptage sur quatre fronts et un rapport de réduction de 1:30 produisent 11 × 4 × 30 = 1320 comptes par tour de l'arbre de sortie. Quand la fréquence des impulsions atteint plusieurs milliers par seconde, le code par interruption accapare une part notable du temps du processeur, tandis que l'unité PCNT de l'ESP32 assure ce comptage sans effort. Les encodeurs optiques industriels fonctionnent le plus souvent en 5–24 V avec des sorties à collecteur ouvert ou différentielles ; vérifie toujours le type de sortie et la tension dans la fiche technique avant de les brancher.
Idées de projets avec un encodeur rotatif
- Bouton de volume numérique : Règle avec l'encodeur le volume de la musique d'un DFPlayer Mini et appuie sur l'axe pour passer au morceau suivant.
- Lampe à sélection de couleur : Fais tourner la teinte d'un anneau de LED WS2812B avec l'encodeur et appuie pour passer en mode luminosité.
- Positionnement précis : Fais avancer un moteur pas à pas d'un nombre de pas fixe à chaque cran ; construis un slider pour caméra ou un plateau tournant.
- Minuteur de cuisine : Règle la durée avec l'encodeur, appuie pour lancer le compte à rebours et fais sonner un buzzer à la fin.
- Manette de jeu : Utilise l'encodeur comme raquette dans Pong ; dans les projets de jeux Arduino, il offre un contrôle plus fin qu'un joystick.
- Réglage de l'heure et de l'alarme : Règle l'heure et l'alarme d'un module horloge RTC avec un seul bouton.
- Façade d'appareil de test : Modifie par petits pas une fréquence PWM, la valeur d'un générateur de signaux ou la température cible d'une station de soudage.
Erreurs fréquentes et solutions
| Problème | Cause probable | Solution |
|---|---|---|
| Compte 2 ou 4 par cran | Chaque front est compté séparément ou les contacts rebondissent | Divise le nombre de fronts par 4, utilise une table d'états, ajoute des condensateurs de 10 à 100 nF sur CLK et DT |
| Ne compte qu'un cran sur deux | Encodeur à 30 crans / 15 impulsions ; chaque cran correspond à un demi-cycle | Divise la valeur de la bibliothèque par 2 au lieu de 4 ; dans le code à table, remplace if (ab == 3) par if (ab == 3 || ab == 0) |
| Sens inversé | CLK et DT inversés ou module différent | Permute les numéros de broche de CLK et DT dans le code |
| Des pas se perdent en tournant vite | Lecture par scrutation, delay dans loop | Utilise les interruptions ou le PCNT de l'ESP32, garde l'ISR courte |
| Le bouton s'enclenche tout seul | Broche SW en l'air, sans pull-up | Utilise pinMode(SW, INPUT_PULLUP) |
| Le compteur saute au hasard | GND non commun, fils longs ou mal enfoncés | Relie les masses, raccourcis les fils, vérifie les contacts de la breadboard |
| fatal error: driver/pulse_cnt.h: No such file or directory / undefined reference to 'pcnt_new_unit' | Cœur Arduino ESP32 2.x (première erreur) ou ESP32-C3, qui n'a pas d'unité PCNT (seconde erreur) | Mets le cœur à jour en 3.x dans le gestionnaire de cartes ou utilise la bibliothèque ESP32Encoder ; sur le C3, utilise le code par interruption |
| Le LCD n'affiche que des carrés | Contraste ou adresse I2C incorrects | Tourne le potentiomètre au dos, essaie l'adresse 0x3F au lieu de 0x27 |
Un encodeur rotatif est l'un des moyens les moins chers de donner à un projet une interface utilisateur professionnelle : un seul bouton et un écran peuvent gérer des dizaines de réglages. Commence par le code par scrutation pour comprendre la logique, passe ensuite à la version par interruption ou PCNT et adapte l'exemple de menu à ton propre projet. Dans l'application BlueGrays, tu trouveras des projets de commande de servo par potentiomètre, d'écran LCD I2C, de bouton-poussoir et de compteur 7 segments avec schémas et code pas à pas, et tu pourras facilement les améliorer en remplaçant le potentiomètre ou le bouton par un encodeur.
Questions fréquentes
Quelle est la différence entre un encodeur rotatif et un potentiomètre ?
Un potentiomètre fournit une tension analogique qui dépend de la position de l'axe, sa rotation est limitée à environ 270–300 degrés et il se lit avec analogRead. Un encodeur rotatif tourne sans fin et produit des impulsions numériques à chaque cran ; il indique le sens et l'amplitude du mouvement, pas la position. L'encodeur convient mieux aux menus et aux réglages par pas, le potentiomètre aux réglages simples liés à une position.
Pourquoi mon encodeur rotatif compte deux pas par cran ou en saute ?
Compter trop vient généralement des rebonds des contacts ou du comptage séparé de chaque front ; diviser les 4 fronts de chaque cran par 4 et utiliser une table d'états règle le problème. Les pas sautés apparaissent quand des impulsions arrivent pendant que loop est occupée. Branche CLK et DT sur des broches d'interruption, lis-les par interruption et ajoute si besoin des condensateurs de 10 à 100 nF : le problème disparaît.
L'encodeur rotatif KY-040 fonctionne-t-il en 3,3 V ?
Oui. Le KY-040 ne contient que des interrupteurs mécaniques et des résistances de pull-up, il fonctionne donc parfaitement en 3,3 V. Sur l'ESP32 et la Raspberry Pi Pico, relie impérativement la broche + au 3V3 ; en 5 V, les pull-up tirent CLK et DT à 5 V et la limite des entrées 3,3 V est dépassée.
Combien d'impulsions par tour donne le KY-040 ?
Les modules KY-040 courants ont 20 crans par tour, et les lignes A et B effectuent un cycle complet à chaque cran. Chaque cycle comporte quatre fronts : on compte donc 80 fronts par tour et chaque cran vaut 18 degrés. Certains encodeurs ont 30 crans et 15 impulsions ; sur ceux-là, chaque cran correspond à un demi-cycle.
Peut-on lire un encodeur rotatif sans interruption ?
Oui, par scrutation (polling), en vérifiant sans cesse la broche CLK dans loop, et c'est souvent suffisant pour un bouton de menu tourné lentement. Mais si loop contient des delay, des mises à jour d'écran ou de longs calculs, les crans tournés rapidement sont perdus. Pour un résultat fiable, on recommande les interruptions et, sur l'ESP32, le compteur matériel PCNT.
Peut-on brancher plusieurs encodeurs rotatifs sur un Arduino ?
Oui. Comme seules D2 et D3 offrent une interruption matérielle sur l'Uno, tu peux, avec la bibliothèque Encoder, relier la première broche de chaque encodeur (celle passée en premier au constructeur) à une broche d'interruption et l'autre à une broche normale, ou utiliser les interruptions sur changement de broche (pin change interrupt). Le Mega a six broches d'interruption, l'ESP32 classique peut compter 8 encodeurs en matériel grâce à ses 8 unités PCNT, et sur la Pico chaque GPIO peut déclencher une interruption.
Projets à réaliser après cet article
Voir tous les projets
Allumer et éteindre une LED avec un bouton
Un appui et la lumière s'allume, un deuxième et elle s'éteint, exactement comme l'interrupteur de ta chambre. L'Arduino remarque chaque appui et fait passer la LED à l'état inverse, en attendant un co

ESP32 Contrôle de LED avec bouton (Toggle)
Chaque fois que l'on appuie sur le bouton, la LED change d'état : si elle est allumée, elle s'éteint, si elle est éteinte, elle s'allume. Modèle classique de basculement. Avec le debounce, nous nettoyons le bruit mécanique du bouton-poussoir pour garantir un seul basculement avec une seule pression.

Raspberry Pi Pico Bouton Debounce : IRQ vs Software Polling (GP5 + GP4)
Une pression, le code compte 3-4 ? C'est le contact bounce ! 🔁 Compare 2 méthodes de comptage dans Serial : RAW polling (attrape bounce, compte faux) vs IRQ + software debounce (précis). Apprends physique du bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handlers, sécurité des variables globales ISR.

Utilisation d'un servomoteur avec un potentiomètre
Quand tu tournes le bouton du potentiomètre, le bras du servomoteur tourne avec lui : un bout du bouton correspond à zéro degré, l'autre à un demi-tour. L'Arduino lit sans arrêt la position du bouton,

Luminosité d'une LED avec un Potentiomètre sur ESP32 (Analogique → PWM)
Dans ce projet, nous contrôlons la luminosité d'une LED de manière analogique à l'aide d'un potentiomètre. La broche ADC de l'ESP32 lit la tension entre 0 et 3,3 V provenant du potentiomètre, puis cette valeur est convertie en signal PWM (modulation de largeur d'impulsion) envoyé à la LED. Ainsi, la LED s'allume et s'éteint progressivement. Avec Arduino Core 3.0+, `analogWrite()` est utilisable directement; sur ESP32-S2/S3, la résolution de l'ADC est fixée à 12 bits.

Compteur avec afficheur 7 segments et boutons
Un compteur tout simple à trois boutons : avec l'un, le chiffre affiché monte d'un cran, avec l'autre il descend d'un cran, et le troisième le remet à zéro. L'afficheur 7 segments à un seul chiffre co

Utilisation de l'écran LCD avec I2C
Offre un écran à ton Arduino et fais-lui écrire son premier message. L'écran LCD à deux lignes se branche avec seulement quelques fils ; un bonjour s'affiche sur la ligne du haut, puis un deuxième tex

Jeu de Raquettes - SSD1306
Une balle, deux raquettes et toi : tu joues au tennis de table, l'un des tout premiers jeux vidéo, sur un petit écran OLED. Deux boutons font monter et descendre ta raquette à droite, tandis que l'ord
Autres articles
Application BlueGrays
Réalise ce montage pas à pas sur ton téléphone
Schéma, code et liste des composants sur un seul écran. Plus de 160 projets Arduino, ESP32 et Pico à réaliser.
Scanne avec ton téléphone
Pointe l’appareil photo vers le code : la boutique d’applications de ton téléphone s’ouvre.
ou ouvre la boutique

