Easy Arduino Menus for Rotary Encoders

59,168

111

53

Published

Introduction: Easy Arduino Menus for Rotary Encoders

Rotary encoders with centre-push buttons are cool input hardware for projects, especially if you want to be able to scroll through menus and select options. Hopefully this Instructable will give you the confidence to try a basic menu system out and combine it with a rotary encoder to give a professional feel to your next project!

Why menus and rotary encoders need code

I wanted to have a menu in an upcoming project and use a rotary encoder with a centre push button as my input hardware. This is really similar to the LCD controllers for RAMPS and Arduino MEGA 3D printers. The rotary encoder will allow me to scroll through the menu options, i.e. navigate the menu, select sub-menus and also change values within sub-menus/settings - very versatile for one hardware interface! A microcontroller is needed to manage all of this and microcontrollers need instructions, AKA code!

Other options

The problem I had with existing Arduino menu libraries and menu code is that for simple menus they were overly complicated. Another drawback of many alternatives was that the code was designed for LCD screens and momentary push buttons, not rotary encoders and adaptable to other display outputs. These menus were geared around selecting between a small number of modes and incrementing values relatively slowly. We know that rotary encoders are a great hardware input option because they afford relatively fast input value changes while retaining fine control at slow speed. I wanted to write code which would allow unambiguous top level menu navigation but also allow you to quickly scroll through a large range of values within each sub-menu/setting, exploiting the strengths of the rotary encoder.

The approach

I decided to follow some advice to use if() statements for a simple menu structure and keep it sketch-based. The resultant code builds on my previous Instructable which sought to reliably read the rotation pulses and direction. Please check it out for background.

In this sketch, we add the reading of the centre push button on the rotary encoder shaft, using code that Nick Gammon developed to record button state changes with debouncing and without relying on the Arduino's delay() function that prevents the microcontroller from executing other code and would potentially introduce noticeable delay in our sketch, e.g. slow display refresh rates. Button state change code is much more useful than just reading digital logic high or low when using a button to select something once, like a menu option, as it can help you prevent unintentional multiple selections for each button press.

Let's take a look at what you need to set up to use this example code in Step 1.

Step 1: Preparation

If you haven't yet, please see my other Instructable on rotary encoder reading to find out how to set up your hardware and Arduino IDE software.

Hardware

The additional hardware connections you need to make use of centre push button are shown in the pictures. I used Fritzing to draw the diagram but it didn't have a rotary encoder component which represented the most likely pin layout, so just use that diagram in conjunction with the notes and look at the photo of the rotary encoder to see what you are more likely to be looking for in terms of rotary encoder pin layout.

One of the two pins on one side of the rotary encoder (as opposed to the side with three pins) needs to be connected to ground and another to a digital pin on the Arduino. I have used D4 for the example sketch. If you choose a different pin, don't forget to change the value of buttonPin in the sketch.

Next comes the code in Step 2.

Step 2: Code

This is the code. By looking at the structure and the comments I hope you will find it easy to adapt for your specific needs!

/*******Interrupt-based Rotary Encoder Menu Sketch*******
 * by Simon Merrett, based on insight from Oleg Mazurov, Nick Gammon, rt and Steve Spence, and code from Nick Gammon
 * 3,638 bytes with debugging on UNO, 1,604 bytes without debugging
 */
// Rotary encoder declarations
static int pinA = 2; // Our first hardware interrupt pin is digital pin 2
static int pinB = 3; // Our second hardware interrupt pin is digital pin 3
volatile byte aFlag = 0; // let's us know when we're expecting a rising edge on pinA to signal that the encoder has arrived at a detent
volatile byte bFlag = 0; // let's us know when we're expecting a rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set)
volatile byte encoderPos = 0; //this variable stores our current value of encoder position. Change to int or uin16_t instead of byte if you want to record a larger range than 0-255
volatile byte oldEncPos = 0; //stores the last encoder position value so we can compare to the current reading and see if it has changed (so we know when to print to the serial monitor)
volatile byte reading = 0; //somewhere to store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent
// Button reading, including debounce without delay function declarations
const byte buttonPin = 4; // this is the Arduino pin we are connecting the push button to
byte oldButtonState = HIGH;  // assume switch open because of pull-up resistor
const unsigned long debounceTime = 10;  // milliseconds
unsigned long buttonPressTime;  // when the switch last changed state
boolean buttonPressed = 0; // a flag variable
// Menu and submenu/setting declarations
byte Mode = 0;   // This is which menu mode we are in at any given time (top level or one of the submenus)
const byte modeMax = 3; // This is the number of submenus/settings you want
byte setting1 = 0;  // a variable which holds the value we set 
byte setting2 = 0;  // a variable which holds the value we set 
byte setting3 = 0;  // a variable which holds the value we set 
/* Note: you may wish to change settingN etc to int, float or boolean to suit your application. 
 Remember to change "void setAdmin(byte name,*BYTE* setting)" to match and probably add some 
 "modeMax"-type overflow code in the "if(Mode == N && buttonPressed)" section*/

void setup() {
  //Rotary encoder section of setup
  pinMode(pinA, INPUT_PULLUP); // set pinA as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  pinMode(pinB, INPUT_PULLUP); // set pinB as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  attachInterrupt(0,PinA,RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below)
  attachInterrupt(1,PinB,RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)
  // button section of setup
  pinMode (buttonPin, INPUT_PULLUP); // setup the button pin
  // DEBUGGING section of setup
  Serial.begin(9600);     // DEBUGGING: opens serial port, sets data rate to 9600 bps
}

void loop() {
  rotaryMenu();
  // carry out other loop code here 
}

void rotaryMenu() { //This handles the bulk of the menu functions without needing to install/include/compile a menu library
  //DEBUGGING: Rotary encoder update display if turned
  if(oldEncPos != encoderPos) { // DEBUGGING
    Serial.println(encoderPos);// DEBUGGING. Sometimes the serial monitor may show a value just outside modeMax due to this function. The menu shouldn't be affected.
    oldEncPos = encoderPos;// DEBUGGING
  }// DEBUGGING
  // Button reading with non-delay() debounce - thank you Nick Gammon!
  byte buttonState = digitalRead (buttonPin); 
  if (buttonState != oldButtonState){
    if (millis () - buttonPressTime >= debounceTime){ // debounce
      buttonPressTime = millis ();  // when we closed the switch 
      oldButtonState =  buttonState;  // remember for next time 
      if (buttonState == LOW){
        Serial.println ("Button closed"); // DEBUGGING: print that button has been closed
        buttonPressed = 1;
      }
      else {
        Serial.println ("Button opened"); // DEBUGGING: print that button has been opened
        buttonPressed = 0;  
      }  
    }  // end if debounce time up
  } // end of state change

  //Main menu section
  if (Mode == 0) {
    if (encoderPos > (modeMax+10)) encoderPos = modeMax; // check we haven't gone out of bounds below 0 and correct if we have
    else if (encoderPos > modeMax) encoderPos = 0; // check we haven't gone out of bounds above modeMax and correct if we have
    if (buttonPressed){ 
      Mode = encoderPos; // set the Mode to the current value of input if button has been pressed
      Serial.print("Mode selected: "); //DEBUGGING: print which mode has been selected
      Serial.println(Mode); //DEBUGGING: print which mode has been selected
      buttonPressed = 0; // reset the button status so one press results in one action
      if (Mode == 1) {
        Serial.println("Mode 1"); //DEBUGGING: print which mode has been selected
        encoderPos = setting1; // start adjusting Vout from last set point
      }
      if (Mode == 2) {
        Serial.println("Mode 2"); //DEBUGGING: print which mode has been selected
        encoderPos = setting2; // start adjusting Imax from last set point
      }
      if (Mode == 3) {
        Serial.println("Mode 3"); //DEBUGGING: print which mode has been selected
        encoderPos = setting3; // start adjusting Vmin from last set point
      }
    }
  }
  if (Mode == 1 && buttonPressed) {
    setting1 = encoderPos; // record whatever value your encoder has been turned to, to setting 3
    setAdmin(1,setting1);
    //code to do other things with setting1 here, perhaps update display  
  }
  if (Mode == 2 && buttonPressed) {
    setting2 = encoderPos; // record whatever value your encoder has been turned to, to setting 2
    setAdmin(2,setting2);
    //code to do other things with setting2 here, perhaps update display   
  }
  if (Mode == 3 && buttonPressed){
    setting3 = encoderPos; // record whatever value your encoder has been turned to, to setting 3
    setAdmin(3,setting3);
    //code to do other things with setting3 here, perhaps update display 
  }
} 

// Carry out common activities each time a setting is changed
void setAdmin(byte name, byte setting){
  Serial.print("Setting "); //DEBUGGING
  Serial.print(name); //DEBUGGING
  Serial.print(" = "); //DEBUGGING
  Serial.println(setting);//DEBUGGING
  encoderPos = 0; // reorientate the menu index - optional as we have overflow check code elsewhere
  buttonPressed = 0; // reset the button status so one press results in one action
  Mode = 0; // go back to top level of menu, now that we've set values
  Serial.println("Main Menu"); //DEBUGGING
}

//Rotary encoder interrupt service routine for one encoder pin
void PinA(){
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's values
  if(reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    encoderPos --; //decrement the encoder's position count
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation
  sei(); //restart interrupts
}

//Rotary encoder interrupt service routine for the other encoder pin
void PinB(){
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's values
  if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    encoderPos ++; //increment the encoder's position count
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation
  sei(); //restart interrupts
}
// end of sketch!

I have used "DEBUGGING" at the start of every comment on any line which isn't critical for the menu to do its thing. If you are happy with the menu's function, you might want to comment out or delete these lines for smaller compiled sketch size.

Be aware that a key part of menu navigation is feedback to the user while they are scrolling through the option and settings. Therefore if you choose not to include the DEBUGGING lines, you should probably use another visual indicator (e.g. LCD text display, LEDs) that encoder inputs are navigating the menu and changing settings.

If I comment out the DEBUGGING lines (noting that some visual feedback would still be needed for menu navigation) the compiled code is around 1,650 bytes for the Arduino Uno, hopefully leaving plenty of room on your ATMEGA328P for the more exciting parts of your sketch!

Go to Step 3 to find out how the menu system works.

Step 3: Operation and Conclusion

Operation

If you open the serial monitor in Arduino after uploading this sketch, and start to turn the encoder shaft, you should see the top level menu rotating through the number of sub-menus/options you have (limited using the modeMax variable). If you press the centre-push button you will see that the mode/sub-menu you have scrolled to is selected and now you have free reign to scroll through 0-255 values in that sub-menu. Now, if you press the centre-push button you will set that value to setting1 or setting2 or setting3 etc. The Arduino automatically and instantaneously returns you to the top level menu once this has happened.

While powered up, the Arduino remembers what you set each setting to and if you go back to the sub-menu for a setting you have already set a value to, it will start your encoder adjustments from the last value you chose!

Conclusion

I set out to write some sketch-based code that would allow rotary encoders to navigate a basic menu for Arduinos. I also tried to make it readable so that, unlike some alternatives, someone could see the menu structure and know what changes to the code they would need to make to tailor the menu to their needs.

This code is basic and generic, specifically to demonstrate the functionality while being easily adaptable to your own application. It uses the serial monitor as a basic debugging tool which also removes the need for a separate display if you want to see how the code works. I hope you find it useful and are inspired to edit, adapt and improve it!

Please share anything you do with it in the comments!

3 People Made This Project!

Recommendations

  • Oil Contest

    Oil Contest
  • Clocks Contest

    Clocks Contest
  • Casting Contest

    Casting Contest
user

We have a be nice policy.
Please be positive and constructive.

Tips

2 Questions

0

Hi Mr. Simon. How i can contact you please give me email address
Thanks regards

If it's a message you don't want to put in the comments, I think you can send me a private message using your Instructables account.

Your code was very helpful! I have my final year project which is of a data logger system. So basically we are planning to interface oled, Bluetooth module, rtc, sd card module, rotary encoder and serial connection with arduino! Could you help us with the code?

53 Comments

How would one go about adding a sub menu with settings in the submenu, Such as hours and minutes in a "set time" sub menu?
main
-set time
---hours
---mins
-set alarm
---hours
---mins

3 replies

Nevermind,.. I figured it out,. Created another variable called submode and just mirrored the structure of the mode selection, in submode selection for each mode.
Can post code if anyone needs it.

Bullfrogerwytsch,

Can you share your code ?

Bullfrogerwytsch, glad you got it sorted and although I don't have an immediate application for your menu I'm sure it would be helpful for others. Please post in the comments! Thanks.

Hi Simon, Awesome article, iv been strugaling with rotary encoders for ages and this really helped! I still have a problem tho (my coding ability is tiny)...

I'm trying to get the code to do a keyboard.press on a clockwise rotation, a different one on counter-clockwise and if possible (stretch goal) a third different keyboard.press on the button press.

I have found putting the keyboard.press / keyboard release lines in the void "rotarymenu" section after the

"if buttonstate ==low" argument (just under serial.println ("button closed"))

works for that keypress (a for example) in both directions of the dial, but I cant figure out how to get it so it it only does a for clockwise and a different key (b) for the opposit direction. Is it just a case of putting the kreypress in the right sections or will i need to write entirely new arguments?

Let's say I want to increase/decrease two different values, one by simply rotating the knob and the other by rotating while pressing the pushbutton: How do I code that? I'm banging my head to the wall, I can't figure it out by myself.

1 reply

Hi MarcoM280, I would declare a further variable "encoderPosButton". Then, instead of:
"if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
encoderPos ++; //increment the encoder's position count"
Use:
"if (reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
if (buttonPressed) encoderPosButton --; //decrement the encoder's button closed position count
else encoderPos --; //decrement the encoder's button open position count"

Then, you need to do the same changes to the encoderPos increment section:
"if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
if (buttonPressed) encoderPosButton ++; //increment the encoder's button closed position count
else encoderPos ++; //increment the encoder's button open position count"

You can get rid of most of the menu code as this only relies on the encoder reading BUT you need to keep the buttonPressed code! Good luck - I haven't tested this but it should help you get round your mental block!

Hi, very interesting project and a really good base to start using Rotary Encoders, lovely code without hardware debouncing. It might be interesting to others that the preferred syntax has changed for declaring the interrupts. You need to use the following updated code if you want this project to work on the Nano (although it will work 'as is' on the Uno).

attachInterrupt(digitalPinToInterrupt(2),PinA,RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below)

attachInterrupt(digitalPinToInterrupt(3),PinB,RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)

I would have loved to have embedded this in a code window, but it looks like I am too lowly to have access to the full editor, or comments are not allowed to have embedded code! Keep up the good work.

1 reply

Thanks 400 Hz! Good to keep people up to date. I'm going to leave the code in the Instructable the same for now but will consider changing it if people have issues.

Thanks Christian!

Hi Simon,

I've been trying without much success to integrate your encoder routine into a library, well, actually merge it with the button routines of the 'clickencoder' library for use in my menu system. That button routine is great for single, double, hold, long hold functions on the button, yet the encoder routine is sporadic, glitchy, the usual ain't going to work for me. lol. As I stated in your other instructable, this encoder is so very well behaved I really enjoy it, and it works great for an old guy who is shaky like me.

1 reply

kd6oji, I'm not sure how to put this into a library. Can't you just use the clickbutton library from https://github.com/pkourany/clickButton/tree/master/src and keep the encoder code in the main sketch?

Hi Simon, thank you for sharing your project.
I have it running using a rotary encoder and Serial window.
I'm trying to figure out how to add submenus (eg. Mode1 could have submenus Mode1SubMode1, Mode1SubMode2, etc) Could you suggest how to do this?
Also when first running the code, it prints the encoderPos, I'm trying to get it to print Menu Names for each position, somewhat unsuccessfully.

1 reply

Hi coffs, sorry I didn't reply earlier. I haven't tried this but I believe it is possible to set up sub menus by placing one in each setting. It would lead to significantly more case/switch code unless you can modularise it.

Hi SimonM83, I have your code working using a rotary encoder and the Serial window. Thank you for sharing your work.
From what I can figure out by looking at the code and laying with the rotary encoder, the menu is a single layer (i.e. it does not use sub-menus). Is there a way to implement sub-menus?
Also, from what I can tell Mode0 doesn't seem to allow changing values. Is it intended as a "Main Menu" type entry?

hi simon

first of all let me tell you , thank you so much by share the code , i think is really awesome how you made it , but i would like know if you could help me , i am writing with your code an irrigation system , but i want to have inside the submenus numbers since 0 to 10 and not 1 to 255 i have not known how to do it , please i really apreciate if could help me with that...this is my code https://drive.google.com/drive/folders/0B9LSKG78XbpyREw2T0stU3ZpQlE?usp=sharing .....thank u so much.

1 reply

Hi alfabcd6, if you look at the code you can see modeMax and this sets the maximum value of the main menu. It sounds like you want to set the maximum value of your submenus. If so, you can add in modeMax1, modeMax2, modeMax3 etc. Then, in each case also include the modeMax code, as in: if(encoderPos > modeMax1+10){encoderPos = modeMax1};
and:
if(encoderPos > modeMax1) {encoderPos = 0};
and then do that for each submenu. I hope that helps - let me know.

Hi SimonM83

Problem solved. I got the library structure sorted. And, the key to the compiler issue was the need to edit the .h code to select the screen 128x64, which was commented out, and the 128x32 was live. This is sad, since the top lines of comment talks to being for 128x64 !!! You would think they would make that live as default. Anyway, thanks so much for all your email support. To all out there, just stay focussed and committed, humble enough to ask and grateful when someone, like Simon and others, offer appreciated help. Off to code my first lines of text !!!

Hi again. No, at least not on purpose. I am sure I'm not using the downloads folder for my arduino sketches. To be honest, the file structure is so screwed up, I wonder if I should delete everything my by only .ino that matters and start over. Thing is, I don't recall (memory issues aren't helping me), where or how I even did it. Can you point to a resource of what the file structure should be? I've learned that for each .ino sketch, there needs to be a folder of the same root name. I assume nothing else goes in that folder. Just the one file. So where then do these library and 1396 files go. This is where I think the problem is. Not only is adafruit a bit vague in that they say download and unzip, rename and that's probably where I went wrong. Trying now to google help for file folder structure, but your advice is even more appreciated.