Could someone please help me to use a rotary encoder with a Menu?

Hi, I'm very new to Arduino but I'm picking everything up quickly. The wiring and electronics part comes pretty easy to me, and I understand some simple coding. I've come pretty far in my project already (a PID temperature controller/water heater) but I want to start adding a menu system. I've found several libraries specifically for LCD menus but I can't figure out how to use the rotary encoder as an input for the menu.

I would like to use a single push-button rotary encoder to navigate a menu tree. I would like to turn left for up, right for down, click to set, double-click to go back and hold to go back to root menu.

In using the ClickEncoder library and has these functions.

If someone could point me in the right direction or show me a compatible menu library I would be very thankful!!

think about your rotary encoder as either changing the voltage delivered to an analogue pin (progression of resistors) or, each position outputting to a different digital pin.

Select the method, measuring the output of the switch with analogRead() (former method) or a digitalRead() of a pin (latter method).

Based on that, use your program to affect the display (State Change would be the way to go).

you can certainly program it in as much time as it takes to decide on the method, wire the hardware and test all the connections (plus or minus a day or two in programming and 10 or so posts to the forum).

I built a menu like that recently but unfortunately it's on my other computer.

Basically what I did was to put all my menu entries into an array. Then I hold a variable as an index to that array and it tells which menu item is selected. The display code uses the same index to decide which one to display. When the button is pressed it uses that same index in a switch case to decide what to do.

I read my encoder with an interrupt and it simply increments or decrements a counter. At the top of each pass through the loop I look at that counter and add it to the index and reset the counter back to zero. I use a modulo expression to make sure the index doesn't go out of bounds.

That's really it. There's not much more to it.

The only tricky part for me was learning to check the button before I check the encoder counter. If the button was pressed then I ignore the encoder count for that loop. That way when the button is pressed it runs the thing that is currently being displayed. Otherwise I might be looking at the first menu item when I press the button, but then the display updates to show the second menu item which wasn't what I was looking at when I pressed the button.

I actually built-out a really neat menu for a past project of mine.
It involved a rotary encoder and LCD screen.
I ended up re-building the code a few times until I was happy with ultimately making it a state-machine.

Here is everything from my project that involved the rotary encoder:

//libraries to include:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// rotary encoder variables:
const byte rotPin1 = 2;  // A
const byte rotPin2 = 3;  // B
volatile int rotPin1Value = 0;
volatile int rotPin2Value = 0;
volatile int oldRotPin1Value = 0;
volatile int oldRotPin2Value = 0;
volatile int rotPosition = 0;
volatile int oldRotPosition = 0;
volatile int rotTurn = 0;
volatile int oldRotTurn = 0;
volatile int rotTurnCount=0;
volatile int rotValue;
// note: this is the value that the rotary encoder function
// is updating with every turn.

// rotary encoder button pin:
const byte rotButton = A3;
// note: This button will be the main button

// keep track of when the button is pressed:
boolean lastRotButtonState;
boolean rotButtonState;
// note: we use these in the edgeDetectRotButton() function.

// to keep track of your state-machines state:
// variables for the state-machine:
boolean sOIntroScreen = false;
boolean sOAnotherMenuScreen = false;
//... and so on and so fourth.

// state-machine (default menu values):
// intro:
byte introChoice = 2;
// 0=ABOUT, 1=OPTIONS, 2=start a game (default)

byte anotherMenuScreen = 1;
// 0=BACK, 1=Whatever, 2=Whatever
//... and so on and so fourth

void setup()
  // set the rotary encoder button pin as an input:
  pinMode(rotButton, INPUT);
  // set the rotary Pin #1 to be an "input":
  pinMode(rotPin1, INPUT);
  // turn on the internal pull-up resistor:
  digitalWrite(rotPin1, HIGH);
  // set the rotary pin #2 to be an "input":
  pinMode(rotPin2, INPUT);
  // turn on the internal pull-up resistor:
  digitalWrite(rotPin2, HIGH);
  //call rotEncoder() when any high/low changed seen
  //on interrupt 0 (pin 2), or interrupt 1 (pin 3) 
  attachInterrupt(0, rotEncoder, CHANGE); 
  attachInterrupt(1, rotEncoder, CHANGE);

void loop()
  // loop through looking for the "S"tate "O"f:
  // note: this a "state machine".
  // intro:
  if(sOIntroScreen == true)
    sOIntro = false;
  // another menu screen:
  if(sOAnotherMenuScreen == true)
    sOAnotherMenuScreen = false;
  //...and so on and so fourth.

// example of one of the "sates" of the state-machine:
void introScreen()
  // setup the LCD screen:
  lcd.print(F("    INTRO SCREEN    "));
  // set the starting rotary encoder value:
  rotValue = introChoice;
  // while the rotary button remains un-pressed:
  while (edgeDetectRotButton() == false)
    // keep the rotary value constrained:
    rotValue = constrain(rotValue, 0, 2);
    // note: 0=ABOUT, 1=OPTIONS, 2=start a game
    // update the LCD screen:
    // 0=ABOUT:
    if(rotValue == 0)
      lcd.print(F(" -     ABOUT      - "));
    // 1=OPTIONS:
    else if(rotValue == 1)
      lcd.print(F(" -    OPTIONS     - "));
    // 2=start a game:
    else if(rotValue == 2)
      lcd.print(F(" -  Start a Game  - "));
  // decide what to do now that the rotary button has
  // been pressed:
  // update the introChoice variable:
  introChoice = rotValue;
  // 0=ABOUT:
  if(introChoice == 0)
    // change the State Of:
    sOAbout = true;
    // get out of this function:
  // 1=OPTIONS:
  else if(introChoice == 1)
    // change the State Of:
    sOSelectOptions = true;
    // get out of this function:
  // 2=start a game:
  else if(introChoice == 2)
    // change the State Of:
    sOSelectTimerType = true;
    // get out of this function:

void rotEncoder()
  // original:
  // interrupt: bildr article:
  // helpful:
  // get the rotary encoder pin values:
  rotPin1Value = digitalRead(rotPin1);
  rotPin2Value = digitalRead(rotPin2);
  // Check if there is any change to either of the pin values:
  if(rotPin1Value != oldRotPin1Value
  || rotPin2Value != oldRotPin2Value)
    // There are four possible rotary-encoder positions for a
    // "quadrature" rotary encoder. These 4 possible positions
    // are between each "click". The reason there are 4 positions
    // between each click instead of just 1 position for each
    // click is so we can quickly detect which direction the rotary
    // encoder is turning.
    // the four posible positions are designated by the following
    // rotary encoder pin values:
    // see:
    // check for the first position:
    if(rotPin1Value == 1 && rotPin2Value == 1)
      // this is the stationary position of the rotary encoder:
      rotPosition = 0;
    // check for the second position:
    else if(rotPin1Value == 0 && rotPin2Value == 1)
      // this is the stationary position of the rotary encoder:
      rotPosition = 1;
    // check for the third position:
    else if(rotPin1Value == 0 && rotPin2Value == 0)
      // this is the stationary position of the rotary encoder:
      rotPosition = 2;
    // check for the fourth position:
    else if(rotPin1Value == 1 && rotPin2Value == 0)
      // this is the stationary position of the rotary encoder:
      rotPosition = 3;
    // set the turn value:
    rotTurn = rotPosition - oldRotPosition;
    // For example: if the old rotary position was 3, and the
    // current rotary position is 2, then the rotary encoder has
    // turned -1 positions (2-3).
    // if turn is indicating 2, then we must have missed a
    // position, and we are no longer able to tell which direction
    // we are turning.
    // check that we didn't miss a positon:
    if(abs(rotTurn) != 2)
      // check for a turn value that indicates clockwise:
      if(rotTurn == -1 || rotTurn == 3)
        // increment the turn count variable:
      // check for a turn value that indicates counter-clockwise:
      else if(rotTurn == 1 || rotTurn == -3)
        // decrement the turn count variable:
    // now check to see if we have made a complete "click" of the
    // rotary encoder: (aka: check whether or not we are back in
    // the stationary position)
    if(rotPosition == 0)
      // check the turn count value to see which direction we have
      // just finished turning:
      if(rotTurnCount > 0)
      else if(rotTurnCount < 0)
      // reset the turn count variable:
      rotTurnCount = 0;

    // log all the positions as old:
    oldRotPin1Value = rotPin1Value;
    oldRotPin2Value = rotPin2Value;
    oldRotPosition = rotPosition;

boolean edgeDetectRotButton()
  // check if the button has been pressed:
  boolean reading = digitalRead(rotButton);
  if(reading != lastRotButtonState)
    lastDebounceTime = millis();
  if ((millis() - lastDebounceTime) > debounceDelay) 
    if(reading != rotButtonState) 
      rotButtonState = reading;
      if(rotButtonState == HIGH) 
        return true;
    return false;
  // set last button state = to reading.
  lastRotButtonState = reading;

if you wanted to see the menu in-action, you can check out this video:

(warning: it’s a long video…)

Hope this helps!
(this is my first contributing post on the forum, all my other posts have been questions)


Thank you all for the responses.

Delta, what you said makes a lot of sense. But unfortunately my C++ knowledge is lacking and I wouldn't know how to set everything up. This is deg the moat complicated part of my project as everything else was fairly simple.

I found a post on here that basically functions that way Id want it to, but they are only snippets and I'm having trouble implementing it into my project.;

I'm getting an error when it comes to Switch(read_encoder())

After a lot of reading I found I had to declare read_encoder with the line: int8_t read_encoder()

It fixed my error but I don't think it's reading the encoder pounds which I'm not sure how to set up.

I've only between able to get the encoder to work with the ClickEncoder library, so this is different then what I have setup currently

Hi, sorry to dig up an old post but i need help to run your code @joshpit2003 !!!

I've watch your video and your menu would be perfect for my little project.

I'm French and newbie and i've some trouble for running your code. Many error when i compile. Could you help me please and give me archive or sketch file.