ESP32 + KY-040 Rotary Encoder Resets and Unresponsive Issues

Hello,

I'm working on a project with an ESP32 board and a KY-040 rotary encoder, and I’m experiencing issues that I’m struggling to resolve. Here’s a summary of my setup and the problem:

  • Hardware Configuration:
    • Rotary Encoder KY-040 connected to the ESP32:
      • CLK -> GPIO 5
      • DT -> GPIO D2
      • SW (Button) -> GPIO 32
    • Power Supply: The encoder and ESP32 are powered by a 5V source in parallel to avoid voltage drops.
  • Library: I’m using the RotaryEncoder library by Matthias Hertel.

Issue Description:

  1. When I turn the knob one step to the right, the displayed number sometimes increments but then freezes, causing the ESP32 to reset.
  2. If I press lightly without completing a step, the counter increases rapidly.
  3. Turning left often causes the program to stop responding entirely.
  4. The button (SW) does not reset the counter as expected.

I'm attaching a video (wasnt able to attach the video due to forum limitations) to illustrate the issue. As I’m new to working with encoders and the ESP32, I suspect the issue might be related to my code. I based my initial code on examples generated by ChatGPT, but I'm not sure if it’s optimized or correct for this setup.

Below is the code I'm currently using in the video but i want to use another code i've taken from another project of a led cube. The idea is to use de encoder to scroll through the diferents options in a menu.

// Objeto para el botón del encoder
OneButton button(BTN_PIN, true);  // `true` para activar el modo pull-up

void resetCounter() {
    // Reinicia el contador cuando se presiona el botón
    counter = 0;
}

#include <Adafruit_SH110X.h>
#include <OneButton.h>

// Pines del rotary encoder KY-040
#define CLK_PIN 5      // Pin CLK del encoder (conectado al puerto 5)
#define DT_PIN 2       // Pin DT del encoder (conectado al puerto D2)
#define BTN_PIN 32     // Pin SW del botón del encoder (conectado al puerto 32)

// Configuración de la pantalla SH1106
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);

// Variables para el contador
int counter = 0;     // Valor actual a mostrar en pantalla
int lastCounter = -1; // Último valor mostrado en pantalla para evitar actualizaciones constantes

// Objeto para el botón del encoder
OneButton button(BTN_PIN, true);  // `true` para activar el modo pull-up

void IRAM_ATTR handleEncoder() {
    // Lee el estado del encoder
    int clkState = digitalRead(CLK_PIN);
    int dtState = digitalRead(DT_PIN);

    // Incrementa o decrementa el contador basado en el estado de CLK y DT
    if (clkState == LOW) {
        if (dtState == HIGH) {
            counter++;
        } else {
            counter--;
        }
    }
}

void resetCounter() {
    // Reinicia el contador cuando se presiona el botón
    counter = 0;
}

void setup() {
    // Configuración de la pantalla SH1106 con los pines de I2C
    Wire.begin(21, 22);            // SDA en el puerto 21, SCL en el puerto 22
    display.begin(0x3C, true);     // Dirección I2C de la pantalla (0x3C)
    display.clearDisplay();
    display.display();

    // Configura los pines del rotary encoder
    pinMode(CLK_PIN, INPUT_PULLUP);
    pinMode(DT_PIN, INPUT_PULLUP);
    pinMode(BTN_PIN, INPUT_PULLUP);

    // Configura la interrupción para el encoder
    attachInterrupt(digitalPinToInterrupt(CLK_PIN), handleEncoder, FALLING);

    // Configura el botón del encoder para reiniciar el contador
    button.attachClick(resetCounter);

    // Muestra el valor inicial en pantalla
    displayNumber();
}

void loop() {
    // Actualiza el botón del encoder
    button.tick();

    // Actualiza la pantalla solo si el valor ha cambiado
    if (counter != lastCounter) {
        displayNumber();
        lastCounter = counter;
    }
}

void displayNumber() {
    // Limpia y muestra el número en la pantalla
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SH110X_WHITE);
    display.setCursor(0, 20);  // Ajusta la posición del texto en pantalla
    display.print("Count: ");
    display.println(counter);
    display.display();
}

Any guidance or suggestions to improve the code or troubleshooting steps would be greatly appreciated!

Thank you in advance for your help.

At last here is the actual code i want to finally use for my project.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Arduino.h>
#include <RotaryEncoder.h>
#include "OneButton.h"

#include <FastLED.h>
#define NUM_LEDS    288
#define LED_PIN     27
#define BRIGHTNESS  170
#define LED_TYPE    WS2812B
#define COLOR_ORDER GRB
uint8_t hue = 0;
uint8_t colorIndex[144];
CRGB leds[NUM_LEDS];            //color data for each led - sets up an array that we can manipulate to set/clear led data.
CRGB leds_buffor[144];          
CRGB leds_segments[12];

#define PIN_IN1 2               //encoder pin
#define PIN_IN2 5               //encoder pin   
#define ROTARYSTEPS 1           // encoder step
#define ROTARYMIN 1             // encoder min
#define ROTARYMAX 10            // encoder max
int lastPos = -1;
int newPos = 1;
int newPos2 = 1;
#define PIN_INPUT 32            //button pin

#define i2c_Address 0x3c //initialize with the I2C addr 0x3C Typically eBay OLED's

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1   //   QT-PY / XIAO
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//---------------

volatile int interrupts;
int totalInterrupts;

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

void IRAM_ATTR onTime() {
  portENTER_CRITICAL_ISR(&timerMux);
  interrupts++;

  portEXIT_CRITICAL_ISR(&timerMux);
}

//-----------------Display start--------
void heart(){
  for (int j=0; j<1; j++){                                          //only one time animation
  for (byte i=0; i<100; i++){
   display.drawBitmap(0, 0, epd_bitmap_allArray[i],64 ,64, SH110X_WHITE);
   display.display();
   delay(10);
   display.clearDisplay();
   
  }
  display.drawBitmap(0, 0, myBitmap,128 ,64, SH110X_WHITE);
  display.display();
  delay(1000);
  display.clearDisplay();
  }
}

// A pointer to the dynamic created rotary encoder instance.
// This will be done in setup()

RotaryEncoder *encoder = nullptr;

// @brief The interrupt service routine will be called on any change of one of the input signals.

IRAM_ATTR void checkPosition()
{
  encoder->tick();                               // just call tick() to check the state.
}

OneButton button(PIN_INPUT, true);
unsigned long pressStartTime;

ICACHE_RAM_ATTR void checkTicks() { 
// include all buttons here to be checked
  button.tick();                                 // just call tick() to check the state.
}

// this function will be called when the button was pressed 1 time only.
void singleClick() {
  Serial.println("singleClick() detected.");
  newPos2 = newPos;
} // singleClick
// this function will be called when the button was held down for 1 second or more.
void pressStart() {
  Serial.println("pressStart()");
  pressStartTime = millis() - 1000; // as set in setPressMs()
} // pressStart()


// this function will be called when the button was released after a long hold.
void pressStop() {
  Serial.print("pressStop(");
  Serial.print(millis() - pressStartTime);
  Serial.println(") detected.");
} // pressStop()

DEFINE_GRADIENT_PALETTE( girlcat_gp ) {            //gradient for pattern usage
    0, 173,229, 51,
   73, 139,199, 45,
  140,  46,176, 37,
  204,   7, 88,  5,
  255,   0, 24,  0};

CRGBPalette16 greenblue2 = girlcat_gp;                


DEFINE_GRADIENT_PALETTE( spellbound_gp ) {          //gradient2 for pattern usage
    0, 232,235, 40,
   12, 157,248, 46,
   25, 100,246, 51,
   45,  53,250, 33,
   63,  18,237, 53,
   81,  11,211,162,
   94,  18,147,214,
  101,  43,124,237,
  112,  49, 75,247,
  127,  49, 75,247,
  140,  92,107,247,
  150, 120,127,250,
  163, 130,138,252,
  173, 144,131,252,
  186, 148,112,252,
  196, 144, 37,176,
  211, 113, 18, 87,
  221, 163, 33, 53,
  234, 255,101, 78,
  247, 229,235, 46,
  255, 229,235, 46};

CRGBPalette16 greenblue = spellbound_gp;

void setup()
{

  //--------------------------------
  // Configure Prescaler to 80, as our timer runs @ 80Mhz
  // Giving an output of 80,000,000 / 80 = 1,000,000 ticks / second
  timer = timerBegin(1'000'000);                
  timerAttachInterrupt(timer, &onTime);    
  // Fire Interrupt every 1m ticks, so 1s
  timerWrite(timer, 5'000'000);
  timerAlarm(timer, 5'000'000, true, 0);     
  //--------------------------------
   FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);        // setup leds
   FastLED.setBrightness(BRIGHTNESS);
  
  Serial.begin(9600);
  while (!Serial)
    ;
  // setup the rotary encoder functionality
  encoder = new RotaryEncoder(PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::TWO03);
  // register interrupt routine
  attachInterrupt(digitalPinToInterrupt(PIN_IN1), checkPosition, CHANGE);
  attachInterrupt(digitalPinToInterrupt(PIN_IN2), checkPosition, CHANGE);

  encoder->setPosition(1 / ROTARYSTEPS);                                     // start with the value of 10.    

    // setup interrupt routine
  attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE);
  button.attachClick(singleClick);
  button.setPressMs(1000);                                                  // that is the time when LongPressStart is called - not in use
  button.attachLongPressStart(pressStart);
  button.attachLongPressStop(pressStop);

  display.begin(i2c_Address, true); // Address 0x3C default
  display.display();
  display.clearDisplay();                                                  // Clear the buffer
  heart();                                                                 
  
for (int i = 0; i < 144; i++) {
    colorIndex[i] = random8();
  }
  
} // setup() close

void screen(){                                                              //funtion for screen operation

   display.setTextSize(1);
   display.setTextColor(SH110X_WHITE);
   display.setCursor(28, 0);
   display.println("LIGHTING MODES");
   display.drawLine(4, 12, 128, 12, SH110X_WHITE);
  switch (newPos) {
    case 0:
     
     break;

    case 1:
      
      //   display.setCursor(6, 20);
      //   display.println("Falling Star");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Rainbow ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Matrix ");
      //  display.setCursor(95, 44);
      //  display.write(30);
      //  display.setCursor(110, 32);
      //  display.write(31);
      break;
      
    case 2:
      display.setCursor(6, 20);
      display.println(" Rainbow ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Matrix ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Spell Pattern ");
      
      break;
      
    case 3:
      display.setCursor(6, 20);
      display.println(" Matrix ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Spell Pattern ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Move Dots ");
      break;
    case 4:
       display.setCursor(6, 20);
      display.println(" Spell Pattern ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Move Dots ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Colour Snake ");
      break;
    case 5:
       display.setCursor(6, 20);
      display.println(" Move Dots ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Colour Snake ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Confetti ");
      break;
    case 6:
       display.setCursor(6, 20);
      display.println(" Colour Snake ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Confetti ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Random Star");
      break;
    case 7:
       display.setCursor(6, 20);
      display.println(" Confetti ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Random Star ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Colorful ");
      break;
    case 8:
       display.setCursor(6, 20);
      display.println(" Random Star ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Colorful ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Beat ");
      break;
    case 9:
       display.setCursor(6, 20);
      display.println(" Colorful ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Beat ");
      display.setTextColor(SH110X_WHITE);
      display.setCursor(6, 44);
      display.println(" Snake ");
      break;
    case 10:
      display.setCursor(6, 20);
      display.println(" Beat ");
      highlight(); 
      display.setCursor(6, 32);
      display.println(" Snake ");
      display.setTextColor(SH110X_WHITE);
     // display.setCursor(6, 44);
     // display.println(" Snake ");
      break;
      
  }
  
  display.display();
  delay(100);
  display.clearDisplay();
}

void highlight(){
  display.setTextColor(SH110X_BLACK, SH110X_WHITE);
  }



//------------------------Main loop--------------

void loop()
{
  static int pos = 0;
  button.tick();
  encoder->tick();                                     // just call tick() to check the state.
  
  newPos = encoder->getPosition() * ROTARYSTEPS;

  if (newPos < ROTARYMIN) {
    encoder->setPosition(ROTARYMIN / ROTARYSTEPS);
    newPos = ROTARYMIN;

  } else if (newPos > ROTARYMAX) {
    encoder->setPosition(ROTARYMAX / ROTARYSTEPS);
    newPos = ROTARYMAX;
  }
  if (lastPos != newPos) {
    Serial.print(newPos);
    lastPos = newPos;
    screen();
  } 
  switch (newPos2) {
    case 0:
    //  Serial.print(newPos);
      break;
    case 1:
      rainbow();
      break;
    case 2:
      matrix();
      break;
     case 3:
      SpellPattern();
      break;
     case 4:
      dots();
      break;
     case 5:
      ColourSnake();
      break;
     case 6:
      confetti();
      break;
     case 7:
      RandomStar();
      break;
     case 8:
      colorful();
      break;
     case 9:
      beat();
      break;
     case 10:
      snake();
      break;
  } 
  //--------
  if (interrupts > 0) {
    portENTER_CRITICAL(&timerMux);
    interrupts--;
    portEXIT_CRITICAL(&timerMux);
    totalInterrupts++;
    Serial.print("totalInterrupts");
    Serial.println(totalInterrupts);
  }
  //--------  
}

Nice picture, but a proper schematic would make it much easier to identify the problem. Start with simple code that just runs a counter; until that works, nothing else will function correctly.

1 Like

Hi, @numaleon
Welcome to the forum.

Can you please post a copy of your circuit, a picture of a hand drawn circuit in jpg, png?
Hand drawn and photographed is perfectly acceptable.
Please include ALL hardware, power supplies, component names and pin labels.

Can I suggest that your forget your code for the moment and JUST write some code for the encoder.
Use a library example to make sure you have proper connections and comms with the device.

chatGPT will not tech you anything with large blocks of code doing many things.

Write you own code in stages and you will learn so much more and make it easier for you to debug YOUR code.

Tom.. :smiley: :+1: :coffee: :australia:

First of all, I want to thank you for your time and help. I'm clearly a beginner in both electronics and programming, but I'm trying to be as self-taught, attentive, and thorough as possible in my work. Please bear with me, and I’ll try to be as specific as possible.

Here’s a list of all the components in my project:

  1. NodeMCU ESP32
  2. Rotary Encoder KY-040
  3. OLED Display 1.3" 128x64 SH1106
  4. 5V 10A Switching Power Supply
  5. Level Shifter (currently only connected to the ESP32; it will be used later for the WS2812B LED strip, which is not yet connected)

All the wires are soldered and protected with heat shrink tubing. The power and ground connections use 18 AWG wires, while the data connections use 24 AWG wires.

As shown in the schematic, I have connected all the components in a parallel configuration to avoid voltage drops.

Hardware Configuration:

  • Rotary Encoder KY-040 connected to the ESP32:
    • CLK -> GPIO 5
    • DT -> GPIO SD2
    • SW (Button) -> GPIO 32
  • OLED Display connected to the ESP32:
    • SCK -> GPIO 22
    • SDA -> GPIO 21

The ESP32 starts up without issues and displays the animations from the code on the OLED screen. The encoder seems to be sending data to the ESP32 because, with each step, the blue light on the ESP32 blinks. However, the problem is that it freezes and resets the ESP32, which then shows the startup animations again.

I tested a simpler code where only a counter is displayed on the screen, and it should increase or decrease based on the encoder’s rotation. When I tested it, turning the knob to the right worked, but I had to be extremely gentle with the steps to increase the count by even 1 to 3 numbers before it resets. If I press lightly on the encoder without completing a step, the number increases rapidly without freezing the ESP32.

I'll upload some photos of the actual project, though it’s a tangle of wires. I hope you can understand it. I think the initial schematic I uploaded is clearer, but if real photos help, here they are, if you want to see in details something more i will upload it as soon as i can. Also i have upload 3 videos to youtube if that helps.

  1. https://youtube.com/shorts/CT0bIqJZfQI
  2. https://youtube.com/shorts/94jb2SAxUlE
  3. https://youtube.com/shorts/GGlH5ZB1KLQ

I'm considering redoing the encoder connections to the ESP32 and using all 18 AWG wires but im not sure if its a problem of the code or of the wireing. This new hobbys is getting tough jajajajaja.

Thank you all in advanced and greetings from argentina!

[grid]



Late to the thread, but I wonder if the ESP32 is resetting because of a power issue. You have both power and ground going to the encoder. Maybe there's a short there when you turn the knob. Does your voltmeter show any sag in the power line when you turn the encoder? Have you checked the pullup resistors on the bottom? They should be 10K.

I think the KY-040 should work without the V+ connection if pullup resistors are enabled on the ESP32. You might test it that way.

Addressing Your Issue:

Your problem is not unexpected, and your wiring may be the root cause. Since hardware is involved, it’s crucial to provide an accurate, annotated schematic of your circuit as it is currently wired. Please note that Fritzing diagrams are not considered proper schematics; they are wiring diagrams and are often not helpful for troubleshooting.

What to problems

  1. Annotated Schematic: Show all connections, including power, ground, and power sources. This helps us understand how your circuit is set up and identify any potential issues.
  2. Technical Information Links: Provide links to technical documentation for each hardware device used in your setup. Avoid links to sales sites like Amazon, as they usually lack the necessary technical details. We need complete specifications to help you effectively.
  3. Additional Information Needed: If the above details are incorrect, more information is required. Tell us what hardware and software you are using, the format of any data (like map data), and how your system determines its position. For example, if your project involves a robot, describe how it navigates and what computers are involved.
  4. Bill Of Materials
    A schematic provides a visual representation of the electrical connections and components in a circuit, showing how they interact and function together, while a Bill of Materials (BOM) is simply a list of components with part numbers and quantities needed for assembly. Unlike a BOM, a schematic illustrates the design’s logic and allows for troubleshooting and understanding circuit behavior. A BOM is useful for sourcing parts but doesn’t convey how they connect or operate in the circuit, which is critical information for understanding and working with electronics.

Why This Matters:

We have no way of knowing the specifics of your setup unless you provide that information. Clear and detailed descriptions enable us to offer the most accurate help possible. Without these details, it’s difficult to diagnose and solve the issues you're experiencing.

1 Like

Hi, @numaleon
Thanks for all the information so far, but as @gilshultz recommends, a schematic would help too.

Can you please post a copy of your circuit, a picture of a hand drawn circuit in jpg, png?
Hand drawn and photographed is perfectly acceptable.
Please include ALL hardware, power supplies, component names and pin labels.

A pen(cil), paper and ruler will make all the difference.

Do you have 10K pull-up resistors on the three outputs of the encoder?
Between each output and 3V3 supply.

Does the problem occur when the top is off the case?
In other words , when the Arduino controller is not positioned over the top of, and close too, the SMPS transformer?

Tom.. :smiley: :+1: :coffee: :australia: