Replacing Rotary Encoder Functionality with a 6-Pin Rocker Switch

Hello, I am working on a network module to control a high-power VFD motor system. Until now, everything in the prototyping stage has worked, although I had to step away from the project for a year and a half. Now that I am implementing the first version of this device, I have encountered the problem of not being able to find a suitable rotary encoder at a fair price. Therefore, I am considering replacing the encoder with a 6-pin rocker switch. Given that the code is around 5000 lines and my peers are no longer involved in the project, it seems easier to adapt the rocker switch to perform the same function as the rotary encoder, which is simply scrolling through the menu.

If anyone has suggestions on how to adapt the code, that would be greatly appreciated. Currently, I am using the CLK pin for the RockerUp pin, the Data pin for the RockerDown pin, and I added an extra push button to simulate the select function. I also had to add some debouncing. At the moment, the code only scrolls in one direction (Up), but pressing the rocker down does not scroll in the opposite direction. I believe this is related to the CW and CCW logic in the code, specifically in the Rotate() function, but I am not entirely sure.

I couldn’t attach the entire code due to its length, but I've included some functions that relate to the CW and CCW logic, which I think might be causing the issue.

// LCD Buttons
#define rockerUpPin  4
#define rockerDownPin 11
#define SelectButton A2
#define BackButton 12
#define MenuButton A5
#define PowerFailure A1

void Rotate();

int previousMenuPosition = 0;
int menuPosition = 0;
bool ESC = false;
bool updateMenu = false;
bool refreshLCD = false;
bool offsetActive = false;
float tempVal = 0;

// LCD Encoder
int previousCLKDown;
int currentCLKDown;

// Menu Buttons (Interrupts)
bool encoderCW;
bool encoderCCW;
bool selectPressed = false;

// Menu Buttons (no interrupts)
bool backPress;
bool previousBackPress;

unsigned long lastDebounceTimeUp = 0;
unsigned long lastDebounceTimeDown = 0;
const unsigned long debounceDelay = 50;  // 50ms debounce dela

void setup(){

// Initialize LCD Menu Buttons
pinMode(PowerFailure, INPUT);          // PowerFailure
pinMode(rockerUpPin, INPUT_PULLUP);     // rockerUpPin 
pinMode(rockerDownPin, INPUT_PULLUP);   // rockerDownPin
pinMode(SelectButton, INPUT_PULLUP);    // SelectButton
pinMode(BackButton, INPUT_PULLUP);      // BackButton
pinMode(MenuButton, INPUT_PULLUP);      // MenuButton
pinMode(30, INPUT);                     // SD card chip detect pin

// Prepare previous state values for the first run through
previousCLKUp = digitalRead(rockerUpPin);    // Initial state of rockerUpPin
previousCLKDown = digitalRead(rockerDownPin); // Initial state of rockerDownPin
backPress = !digitalRead(BackButton);        // Invert value for active low

// Set Interrupts for the rotary encoder and select button on the encoder
attachInterrupt(digitalPinToInterrupt(rockerUpPin), Rotate, CHANGE);           // Interrupt for rockerUpPin
attachInterrupt(digitalPinToInterrupt(rockerDownPin), Rotate, CHANGE);         // Interrupt for rockerDownPin
attachInterrupt(digitalPinToInterrupt(SelectButton), SelectPressed, FALLING);  // Interrupt for SelectButton
attachInterrupt(digitalPinToInterrupt(PowerFailure), PowerLoss, FALLING);      // Interrupt for PowerFailure


  clearScreen();  // Clear the LCD screen

}

// Main menu *****************************************************************
void MainMenu() {
  int menuLength = 6;
  menuPosition = 1;
  ESC = false;
  clearScreen();
  updateMenu = true;
  while (!ESC) {
    previousMenuPosition = menuPosition;
    if (encoderCW) {
      if (menuPosition >= menuLength) {
        menuPosition = 1;
      } else {
        menuPosition++;
      }
      encoderCW = false;
      updateMenu = true;
    } else if (encoderCCW) {
      if (menuPosition <= 1) {
        menuPosition = menuLength;
      } else {
        menuPosition--;
      }
      encoderCCW = false;
      updateMenu = true;
    }
    if ((previousMenuPosition == 4 && menuPosition == 3) || (previousMenuPosition == menuLength && menuPosition == 1) || (previousMenuPosition == 3 && menuPosition == 4) || (previousMenuPosition == 1 && menuPosition == menuLength)) {
      refreshLCD = true;  // Checking to see if the menu has scrolled
    }
    if (updateMenu) {
      if (refreshLCD) {
        clearScreen();
        refreshLCD = false;
      }
      if (menuPosition < 4) {
        setCursor(0x04);
        Serial1.print("-Main Menu-");
        setCursor(0x41);
        Serial1.print("Toggle Offset:");
        setCursor(0x40 + 17);
        if (offsetActive) {
          Serial1.print(" ON");
        } else {
          Serial1.print("OFF");
        }
        setCursor(0x15);
        Serial1.print("Ethernet Info");
        setCursor(0x55);
        Serial1.print("Cable Settings");
      } else if (menuPosition > 3) {
        setCursor(0x01);
        Serial1.print("Drum Settings");
        setCursor(0x41);
        Serial1.print("Set Units:");
        setCursor(0x40 + 16);
        if (metric) {
          Serial1.print(" m");
        } else if (imperial) {
          Serial1.print("ft");
        }
        if (seconds) {
          Serial1.print("/s");
        } else if (minutes) {
          Serial1.print("/m");
        }
        setCursor(0x15);
        Serial1.print("NMEA Period: ");
        LCDPrintSeconds(NMEAPeriod, 0x14);
      }
      DrawCursor();
      updateMenu = false;
    }
    if (selectPressed) {
      switch (menuPosition) {
        case 1:
          LCDToggleOffset(1);
          if (sdCard) {
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("offset:");
            drumFile.print(offset, 1);
            drumFile.print(',');
            drumFile.close();
          }
          break;
        case 2:
          LCDEthernetInfo(2);
          break;
        case 3:
          LCDCableSettings(3);
          break;
        case 4:
          LCDDrumSettings(4);
          break;
        case 5:
          LCDSetUnits(5);
          if (sdCard) {
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("units:");
            drumFile.position();
            if (metric) {
              drumFile.print('M');
            } else if (imperial) {
              drumFile.print('I');
            }
            if (seconds) {
              drumFile.print('s');
            } else if (minutes) {
              drumFile.print('m');
            }
            drumFile.print(",");
            drumFile.close();
          }
          updateLimits = true;
          break;
        case 6:
          NMEAPeriod = LCDSetNMEAPeriod(0x14);
          if (sdCard) {
            tempVal = NMEAPeriod / 1000.0;
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("NMEAPeriod:");
            drumFile.position();
            drumFile.print(tempVal, 1);
            drumFile.print(",");
            drumFile.close();
          }
          break;
      }
      selectPressed = false;
    }
    previousBackPress = backPress;
    backPress = !digitalRead(BackButton);  // Inverting reading because of active low button
    if (backPress && (backPress != previousBackPress)) {
      ESC = true;
      delay(50);
    }
  }
}

// Cursor *****************************************************************
void DrawCursor() {
  //Clear display's ">" parts
  setCursor(0x0);      //1st line, 1st block
  Serial1.print(" ");  //erase by printing a space
  setCursor(0x40);
  Serial1.print(" ");
  setCursor(0x14);
  Serial1.print(" ");
  setCursor(0x54);
  Serial1.print(" ");
  //Place cursor to the new position
  switch (menuPosition)  //this checks the value of the menu
  {
    case 0:
    case 4:
      setCursor(0x0);  //1st line, 1st block
      Serial1.print(">");
      break;
    //-------------------------------
    case 1:
    case 5:
      setCursor(0x40);  //2nd line, 1st block
      Serial1.print(">");
      break;
    //-------------------------------
    case 2:
    case 6:
      setCursor(0x14);  //3rd line, 1st block
      Serial1.print(">");
      break;
    //-------------------------------
    case 3:
    case 7:
      setCursor(0x54);  //4th line, 1st block
      Serial1.print(">");
      break;
  }
}

void LCDEthernetInfo(int menuReturn) {
  selectPressed = false;
  ESC = false;
  clearScreen();
  setCursor(0x02);
  Serial1.print("-Ethernet Info-");
  setCursor(0x40);
  if ((Ethernet.localIP() == error0IP) || (Ethernet.localIP() == error1IP)) {
    Serial1.print("No IP Connection");
  } else {
    Serial1.print(Ethernet.localIP());
  }
  setCursor(0x14);
  if (!manualState) {
    Serial1.print("Wireless Control");
  } else {
    Serial1.print("Local Mode");
  }
  setCursor(0x54);
  if (DHCPError) {
    Serial1.print("DHCP Error!");
  }
  while (!ESC) {
    previousBackPress = backPress;
    backPress = !digitalRead(BackButton);
    if (backPress && (backPress != previousBackPress)) {
      ESC = true;
      delay(50);
    }
  }
  encoderCW = false;
  encoderCCW = false;
  ESC = false;
  updateMenu = true;
  refreshLCD = true;
  menuPosition = menuReturn;
}

void LCDCableSettings(int menuReturn) {
  int menuLength = 6;
  selectPressed = false;
  menuPosition = 1;
  clearScreen();
  updateMenu = true;
  ESC = false;
  while (!ESC) {
    previousMenuPosition = menuPosition;
    if (encoderCW) {
      if (menuPosition >= menuLength) {
        menuPosition = 1;
      } else {
        menuPosition++;
      }
      encoderCW = false;
      updateMenu = true;
    } else if (encoderCCW) {
      if (menuPosition <= 1) {
        menuPosition = menuLength;
      } else {
        menuPosition--;
      }
      encoderCCW = false;
      updateMenu = true;
    }
    if (updateMenu) {
      if ((previousMenuPosition == 4 && menuPosition == 3) || (previousMenuPosition == menuLength && menuPosition == 1) || (previousMenuPosition == 3 && menuPosition == 4) || (previousMenuPosition == 1 && menuPosition == menuLength)) {
        // If the user has scrolled to the next menu
        clearScreen();
      }
      if (menuPosition < 4) {
        setCursor(0x02);
        Serial1.print("-Cable Settings-");
        setCursor(0x41);
        Serial1.print("Max Limit:");
        ConvertMeters(maxCablePayedOut);
        LCDPrintUnitFloat(tempVal, 0x40);
        setCursor(0x15);
        Serial1.print("Min Limit:");
        ConvertMeters(minCablePayedOut);
        LCDPrintUnitFloat(tempVal, 0x14);
        setCursor(0x55);
        Serial1.print("Length:");
        ConvertMeters(cableLength);
        LCDPrintUnitFloat(tempVal, 0x54);
      } else if (menuPosition > 3) {
        setCursor(0x01);
        Serial1.print("Diameter:");
        ConvertCentimeters(cableDiameter);
        LCDPrintUnitFloatCentimeters(tempVal, 0x00);
        setCursor(0x41);
        Serial1.print("Scale:");
        LCDPrintUnitlessFloat(scaleFactor, 0x40);
        setCursor(0x15);
        Serial1.print("Stretch:");
        LCDPrintUnitlessFloat(stretchFactor, 0x14);
      }
      DrawCursor();
      updateMenu = false;
    }
    if (selectPressed) {
      switch (menuPosition) {
        case 1:
          maxCablePayedOut = LCDSetMaxLimit(0x40);
          if (sdCard) {
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("maxCablePayedOut:");
            drumFile.print(maxCablePayedOut, 1);
            drumFile.print(',');
            drumFile.close();
          }
          updateLimits = true;
          break;
        case 2:
          minCablePayedOut = LCDSetMinLimit(0x14);
          if (sdCard) {
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("minCablePayedOut:");
            drumFile.print(minCablePayedOut, 1);
            drumFile.print(',');
            drumFile.close();
          }
          updateLimits = true;
          break;
        case 3:
          cableLength = LCDSetCableLength(0x54);
          if (sdCard) {
            drumFile = SD.open("DRMCONST.TXT", (O_READ | O_WRITE));
            drumFile.find("cableLength:");
            drumFile.position();
            drumFile.print(cableLength, 1);
            drumFile.print(",");
            drumFile.close();
          }
          break;
        case 4:
          cableDiameter = LCDSetCableDiameter(0x00);
          if (sdCard) {
            drumFile = SD.open("drmConst.TXT", (O_READ | O_WRITE));
            drumFile.find("cableDiameter:");
            drumFile.print(cableDiameter, 5);
            drumFile.print(",");
            drumFile.close();
          }
          break;
        case 5:
          scaleFactor = LCDSetScaleFactor(0x40);
          if (sdCard) {
            drumFile = SD.open("drmConst.TXT", (O_READ | O_WRITE));
            drumFile.find("scaleFactor:");
            drumFile.position();
            drumFile.print(scaleFactor, 4);
            drumFile.print(",");
            drumFile.close();
          }
          break;
        case 6:
          stretchFactor = LCDSetStretchFactor(0x14);
          if (sdCard) {
            drumFile = SD.open("drmConst.TXT", (O_READ | O_WRITE));
            drumFile.find("stretchFactor:");
            drumFile.position();
            drumFile.print(stretchFactor, 4);
            drumFile.print(",");
            drumFile.close();
          }
          break;
      }
      selectPressed = false;
    }
    previousBackPress = backPress;
    backPress = !digitalRead(BackButton);
    if (backPress && (backPress != previousBackPress)) {
      ESC = true;
      delay(50);
    }
  }
  ESC = false;
  updateMenu = true;
  refreshLCD = true;
  menuPosition = menuReturn;
}



void SelectPressed() {
  noInterrupts();
  selectPressed = true;
  interrupts();
}

void Rotate() {
    unsigned long currentTime = millis();

    // Logic for rockerUpPin (Clockwise)
    if ((currentTime - lastDebounceTimeUp) > debounceDelay) {
        int currentCLKUp = digitalRead(rockerUpPin);
        if (currentCLKUp == HIGH && previousCLKUp == LOW) {
            encoderCCW = true;  // Clockwise rotation detected
        }
        previousCLKUp = currentCLKUp;      // Update previous state
        lastDebounceTimeUp = currentTime;  // Update debounce time for rockerUpPin
    }

    // Logic for rockerDownPin (Counterclockwise)
    if ((currentTime - lastDebounceTimeDown) > debounceDelay) {
        int currentCLKDown = digitalRead(rockerDownPin);
        if (currentCLKDown == HIGH && previousCLKDown == LOW) {
            encoderCW = true;  // Counterclockwise rotation detected
        }
        previousCLKDown = currentCLKDown;   // Update previous state
        lastDebounceTimeDown = currentTime; // Update debounce time for rockerDownPin
    }
}

Please provide engineering information and schematics.

1 Like

What Arduino board is used?

This may be of some use to you.
https://www.arrow.com/en/research-and-events/articles/encoder-vs-potentiometer-how-to-choose
I hope it is.

I’m not using an Arduino board exactly. I’m using the Automation Direct P1AM-100 PLC, which is Arduino-compatible.

Sure, I’m using the Automation Direct P1AM-100 PLC, which is Arduino-compatible. Along with the P1AM-100 module, I am using the P1AM-GPIO (similar to the image attached below), and some additional modules that are unrelated to my current issue, as they are working perfectly. The only problems I'm encountering are with the user interface and menu/list scrolling.

I was initially using a rotary encoder with 5 pins. One V+ was connected to 5 volts from a power conditioning backup module designed by one of my team members. The CLK pin was connected to GPIO pin 4, the Data pin to GPIO pin 11, the SW pin to GPIO pin A2, and GND to the common ground. Since I needed to change it, I decided to buy a rocker switch from Mouser (PN: 1939.3314), which is a 2-pole, 6-pin switch. I was initially using just one side (1st pole), specifically pins 1, 2, and 3.

The first pin was connected to the pin I was using for CLK, the middle pin was connected to ground, and the third pin was connected to Data. This way, pressing the button upward would close the first contact (between the 1st and 2nd pins), and pressing the button in the other direction would close the second contact (between the 2nd and 3rd pins). I didn't use the other side/pole.

After configuring it this way and addressing some debounce issues (as shown in the code attached), I was able to scroll through the menu, but only in one direction (upward). I need functionality for both directions. I'm unsure how to replicate the original rotary encoder's functionality mechanically or how to edit the code to fix the CW (clockwise) and CCW (counterclockwise) logic. Since the rest of the project is working perfectly, I will omit that part.



Sorry, no knowledge about that device.

The same words.

It does not specify the device.

Hopefully the other helpers keep on.

What one and the documentation for it. The only rotary encoder with 5 pins I am familiar with outputs a 4 bit binary number on 4 pins and the 5th is ground. There are detentes for each step.

The best way to deal with this is to provide a Minimal, Complete and Verifiable Example (How to create a Minimal, Reproducible Example - Help Center - Stack Overflow)

a suitable rotary encoder
What are the specifications of the encoder which make it "suitable"?

Do you have working code with the rotary encoder? If you are just now building this, how has the code been tested?

The code works ok. Here's a slightly modified version running in wokwi.

This version uses no interrupts, just the up/dn switches.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.