3 button menu for 16x2 LCD - help needed

Hi, I'm new to Arduino and I didn't use C++ since I finished my uni 10 years ago.

I have an Arduino Micro and 16x2 I2C LCD. I try to create a menu (6 positions at the moment). Each but last position will have own set of options (in brackets below), example:

SETTINGS
1.Language (Eng, ..., ...)
2.Temp.scale (*C, *F, K)
3.Scr.format (Row, Col)
4.Sensor 1 (LM35, PT100, ...)
5.Sensor 2 (LM35, PT100, ...)
6.Exit // back to main display

I want to use 3 push buttons (I know would be easier with 4) to control menu and options for each setting. Button1 (-/down), button2 (+/up), button3 (enter/exit).

At the moment I have menu done. For now, each menu line as separate function that is called with own parameters in 'switch(menu)..case'. I can scroll down/up using buttons. '6.Exit' works when I press button3.

My problem is, let's say I want to change '1.Language' from 'Eng' to another. I scroll to menu position '1.' using (-/down) (+/up) buttons and press (enter/exit) button once. It displays cursor and I can scroll through all language options with (-/down) (+/up) buttons. Problem is with code to save chosen option and exit to scroll 'Settings' menu again. I tried few different ways to do that. Here are 3 different ways how it works, depending on code I use.

  1. I can display cursor, enter and scroll options, but then I'm stuck scrolling language options, can't save chosen option and exit to scroll menu again; or
  2. I can display cursor, enter and scroll options, but to save and exit I have to press (enter/exit) and either (-/down) or (+/up) button, but that scrolls menu at same time; or
  3. I have to hold (enter/exit) button and press either (-/down) or (+/up) button to change option, but can't enter options to display cursor and scroll them.

Here is part of code I'm working on and declaration of variables. I display two menu lines at once (i.e. '1.(...)' and '2.(...)) but only top one can have options accessed and changed at any given time.

For code testing I temporarily display on LCD values of concerned variables, to see what values they have at a time. Anyway, can find solution yet.

Can you please point me in the right direction how to exit scrolling options and go back to scroll menu? Do I need some more variables or I have to find right combination of those I already use?

Thank you, Amy

bool down_btn = 0,        // state of button '-/down' (0-released, 1-pressed)
     up_btn = 0,          // state of button '+/up'
     enter_btn = 0,       // state of button 'Enter/Exit'
     subMenu = 0,         // menu option (0-not entered, 1-entered)
     start = 0,		  // 'Start' screen (0-not displayed, 1-displayed)
     enter_btn_last_state = 0;

int menu = 0;             // controls which menu line is displayed on top LCD line

void loop()
{
  if (start == 0)   // then display 'Settings' screen (for now, will be welcome screen)
  {
    screen4_1();
    start = 1;
  }
  down_btn = digitalRead(12);
  up_btn = digitalRead(11);
  enter_btn = digitalRead(10);
  delay(75);

  if (enter_btn == 1 && enter_btn_last_state == 0) enter_btn_last_state = 1;

switch (menu)   // menu SETTINGS
{
  case 0:
    if (down_btn == 1 && subMenu == 0)   // down (menu 1. & 2.)
    {
      clrscr();
      screen4_2(0, 'E');
      screen4_3(1, 'C');
      menu = 1;
    }

    // stops carrying enter button state to other menus
    if (enter_btn_last_state == 1) enter_btn_last_state = 0;

  break;

  case 1:   // menu 1.Language
    if (up_btn == 1 && subMenu == 0)   // up ('Settings' screen)
    {
      clrscr();
      screen4_1();
      menu = 0;
    }

    if (down_btn == 1 && subMenu == 0)   // down (menu 2. & 3.)
    {
      clrscr();
      screen4_3(0, 'C');
      screen4_4(1, 'R');
      menu = 2;
    }

    if (enter_btn_last_state == 1)   // button 'enter' pressed, enter options
    {
      lcd.setCursor(15, 0);
      lcd.blink_on();
      subMenu = 1;

      if (down_btn == 1)   // button '-/down' pressed, show previous option
      {
        lcd.home();
        screen4_2(0, 'P');
      }

      if (up_btn == 1)   // button '+/up' pressed, show next option
      {
        lcd.home();
        screen4_2(0, 'E');
      }
    
// without this section I can press (enter/exit) button to display cursor and
// press (-/down) or (+/up) to change option but obviuosly can't exit back to
// scrolling menu.
// I tried few different parameter for 'if' but can't find the right one yet
/*
      if (enter_btn == 1 && enter_btn_last_state == 1 && subMenu == 1)
      {
        lcd.blink_off();
        enter_btn_last_state = 0;
        subMenu = 0;
      }
*/
  break;

  case 2:
	(...)
  break;

Maybe you can glean something from this.  You'll have to adapt the encoder parts to your two button method but that shouldn't be too hard since it's just up/down vs. left/right.

/*
  Demonstrate a simple two level menu system on the serial monitor
  using a quadrature encoder and integral pushbutton.

  Navigation mode uses the encoder to select a variable to change.
  Alter mode uses the encoder to adjust the selected value.
  The encoder pushbutton toggles between navigation and alter modes.

  v8 combines the value adjustment code with the 'build an integer'
  code to adjust a large value one digit at a time. The builtup
  number is displayed following the individual digits.

  Toggling the two booleans is retained and added is the ability for
  one of the bools to control the sign of the number.
*/

/*
   Usage for v 2.7 https://github.com/thomasfredericks/Bounce2/blob/master/README.md
   Demonstrates setPressedState() and getPressedState()
*/

#include <Bounce2.h>  // a library to handle the pushbutton
#define BOUNCE_PIN A0
// Bounce encoderPB = Bounce(); // old style instantiation
Bounce2::Button encoderPB = Bounce2::Button();

// variables having to do with the encoder

const uint8_t INTPIN1 = 3;  // Rotary encoder interrupt on this Arduino Uno pin.
const uint8_t INTPIN2 = 2;  // Rotary encoder interrupt on this Arduino Uno pin.

#include <Rotary.h>  // encoder handler by Buxton
//                   https://github.com/buxtronix/arduino/tree/master/libraries/Rotary
//
volatile int8_t encoderDirection = 0;  //  Encoder direction value, set in the 'rotate' ISR
//                                         0 = no movement, +1 = CW, -1 = CCW

const int8_t numberOfValues = 8;  // total number of variables used / displayed
//                                  this determines when to wrap the 'cursor' position
int8_t valueToAdjust = 0;      // Selects which value will be adjusted
bool isNavigationMode = true;  // controls select vs. alter mode

const int8_t numberOfArrayElements = 5;  // The five array elements represent five digits
//                                         which will be joined, according to their
//                                         respective place values, in largeNumber

int8_t digitNumber[numberOfArrayElements];  // compiler will initialize to zero
byte numElements = sizeof(digitNumber) / sizeof(digitNumber[0]);

bool signIndicator = true, bool2;  // one sign toggle, one toggle switch

const uint8_t modeIndicator = 12;  // Pin number for common anode LED

// enum values for the switch / case construct

enum valueSelect { tenPwr0,
                   tenPwr1,
                   tenPwr2,
                   tenPwr3,
                   tenPwr4,
                   fullNumber,
                   signToggle,
                   switchToggle };

/*
   Rotary encoder pin assignments
*/
Rotary rotary = Rotary(INTPIN1, INTPIN2);  // interrupts are used on both pins 2 & 3
//
// === S E T U P ===

void setup() {
  Serial.begin(115200);
  attachInterrupt(0, rotate, CHANGE);
  pinMode(INTPIN1, INPUT_PULLUP);
  attachInterrupt(1, rotate, CHANGE);
  pinMode(INTPIN2, INPUT_PULLUP);
  encoderPB.attach(BOUNCE_PIN, INPUT_PULLUP);
  encoderPB.interval(5);
  //encoderPB.setPressedState(HIGH); // for library demo only
  pinMode(modeIndicator, OUTPUT);  // Common anode LED
  displayAllValues();
}

void loop() {

  encoderPB.update();  // Refresh the pushbutton object variables

  if (encoderPB.fell()) {                                          // User pushed a button
    isNavigationMode = !isNavigationMode;                          // Toggle navigate/adjust mode
    digitalWrite(modeIndicator, (isNavigationMode ? LOW : HIGH));  // Annunciate mode via LED
    //Serial.println(encoderPB.getPressedState()); // for library demo only
  }

  if (encoderDirection != 0) {  // Test if encoder moved.
    // Select the value of interest
    if (isNavigationMode) {
      selectValue();
    }

    else {
      changeDataValues();
    }

    encoderDirection = 0;  // Reset the increment value
    displayAllValues();    // Update the display
  }
}  // end of loop

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

void changeDataValues() {
  //
  // In data change mode we come here to increment/decrement numeric values
  // and toggle boolean values

  uint8_t switchUse = (numberOfArrayElements - 1) - valueToAdjust;

  switch (valueToAdjust) {
    case tenPwr0 ... tenPwr4:
      digitNumber[switchUse] += encoderDirection;
      if (digitNumber[switchUse] < 0) digitNumber[switchUse] = 0;
      if (digitNumber[switchUse] > 9) digitNumber[switchUse] = 9;
      break;

      // For booleans encoder direction is irrelevant. If we've made
      // it this far the encoder moved so toggle the indicated bool.

    case (signToggle):
      signIndicator = !signIndicator;
      break;

    case (switchToggle):
      bool2 = !bool2;
      break;

    default: break;
  }
}

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

long digitsToNumber() {
  /*
    Build a unified, single integer from separate digits taken
    from an array.

    The array elements (digits) are manipulated individually by
    the changeDataValues menu function
  */
  long largeNumber = 0;
  unsigned long tenXMultiplier = 1;

  for (int8_t i = 0; i < numElements; i++) {
    largeNumber += digitNumber[i] * tenXMultiplier;
    tenXMultiplier *= 10;
  }

  if (signIndicator == false) {
    largeNumber *= -1;
  }
  return largeNumber;
}

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

void selectValue() {
  /*
    If the encoder moved, the value which selects the array element
    to be changed is incremented or decremented accordingly. Values
    wrap in both directions.
  */
  valueToAdjust += encoderDirection;
  if (valueToAdjust >= numberOfValues) valueToAdjust = 0;
  if (valueToAdjust < 0) valueToAdjust = numberOfValues - 1;
}

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

void rotate() {  // Encoder ISR
  /*
    Interrupt Service Routine for rotary encoder:
    An interrupt is generated any time either of the rotary
    inputs change state. Once the interrupt is acted upon,
    other code will clear encoderDirection to zero.
  */
  byte result = rotary.process();
  if (result == DIR_CW) {
    encoderDirection = +1;
  } else if (result == DIR_CCW) {
    encoderDirection = -1;
  }
}

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

void displayAllValues() {
  /*
      Format and print the values and add headings and a position cursor
  */
  for (int8_t i = numberOfArrayElements; i > 0; i--) {
    Serial.print(i);
    Serial.print("\t");
  }
  Serial.print("number\t");
  Serial.print("sign\t");
  Serial.println("bool2");

  for (int8_t i = numberOfArrayElements - 1; i >= 0; i--) {
    Serial.print(digitNumber[i]);
    Serial.print("\t");
  }

  //  Serial.print(unifiedDigits);
  long x14 = digitsToNumber();
  Serial.print(x14);
  Serial.print("\t");

  if (signIndicator) Serial.print("pos");
  else Serial.print("neg");
  Serial.print("\t");

  if (bool2) Serial.print("true");
  else Serial.print("false");
  Serial.println();

  // Print a cursor '--' under the value of interest

  for (byte i = 0; i < numberOfValues; i++) {
    if (i == valueToAdjust) Serial.print("--\t");
    else Serial.print("\t");
  }
  Serial.println();
  // Use F macro i Serial.print to save RAM space
  Serial.println(F("............................................................"));
}

i think you need

  • plus button that wraps around to the beginning
  • accept button to select the currently displayed option
  • exit button to go back to previous menu without accepting an option

Thank you for your reply. I will play with encoders at some point as well. Maybe I will convert my little project to use encoder in the future to learn new things and your code can help me in it. Thank you.

I was thinking about using some variables to register if (enter/exit) button is either in Enter or Exit mode, i.e. when I press button to enter menu item to scroll options it will not only record that button was pressed (like it is now), but also that menu is in 'Enter' mode. Next time I press the button it can't go into 'Enter' mode again, so it has to go into 'Exit' mode resetting 'Enter' variable to be used next time. Something like flip-flop.

If you get stuck with your code and want to use a library that works by defining the menu in data tables, you should look at this one that is designed specifically for 1-2 line LCD modules:

Library MajicDesigns/MD_Menu: Menu system for displays with up to 2 lines (github.com) or install MD_Menu from the IDE library manager. Look for library documentation in the docs folder.

Blog post describing the library A Menu System for LCD Modules – Arduino++ (wordpress.com)

Hi, thank you marco_c. I found that yesterday but didn't take a closer look yet. I'll do that for sure.

I spent few hours today searching online and I found and tested few sketches how to use push button as toggle switch. I found this one very easy to understand and implement to my code.

[use a push button as a toggle switch - #2 by LarryD]

Below is updated code from my 1st. post. Just in case someone needs an idea one day how to do it. Menu works great now with 3 push buttons ('-/down', '+/up', 'enter/exit').

I use 'enter/exit' push button either as toggle switch using 'btn3_state' (see in 'case 1:' in 'enter options') or as push button using 'btn3_prev' (see in 'case 6:' in 'accept and EXIT'). Works really great.

Thank you to all of you for help.

bool down_btn = LOW,      // state of button '-/down' (LOW-released, HIGH-pressed)
     up_btn = LOW,        // state of button '+/up'
     btn3_state = LOW,    // state of button 'Enter/Exit'
     btn3_reading,        // reading state of button 'Enter/Exit'
     btn3_prev = LOW,     // previous state of button 'Enter/Exit'
     start = 0,           // 'Start' screen (0-not displayed, 1-displayed)
     subMenu = 0;         // menu option (0-not entered, 1-entered)

int menu = 0;             // controls which menu line is displayed on top LCD line

unsigned long time = 0,           // the last time the output pin was toggled
              debounce = 200UL;   // the debounce time, increase if the output flickers

void loop()
{
  if (start == 0)   // then display 'Settings' screen (for now, will be welcome screen)
  {
    screen4_1();
    start = 1;
  }

  btn3_reading = digitalRead(10);
  if (btn3_reading == LOW && btn3_prev == HIGH && millis() - time > debounce)
  {
    if (btn3_state == LOW)
      btn3_state = HIGH;
    else
      btn3_state = LOW;

    time = millis();
  }
  btn3_prev = btn3_reading;

  down_btn = digitalRead(12);
  up_btn = digitalRead(11);
  delay(75);

switch (menu)   // menu SETTINGS
{
  case 0:
    if (down_btn == HIGH)   // down (menu 1. & 2.)
    {
      clrscr();
      screen4_2(0, 'E');
      screen4_3(1, 'C');
      menu = 1;
    }

    // stops carrying 'enter' button state to other menus
    if (btn3_state == HIGH) btn3_state = LOW;

  break;

  case 1:   // menu 1.Language
    if (up_btn == HIGH && subMenu == 0)     // up (settings)
    {
      clrscr();
      screen4_1();
      menu = 0;
    }

    if (down_btn == HIGH && subMenu == 0)     // down (menu 2. & 3.)
    {
      clrscr();
      screen4_3(0, 'C');
      screen4_4(1, 'R');
      menu = 2;
    }

    if (btn3_state == HIGH)     // enter options
    {
      lcd.setCursor(15, 0);
      lcd.blink();
      subMenu = 1;

      if (down_btn == HIGH)    // button '-/down' pressed, show previous option
      {
        lcd.home();
        screen4_2(0, 'P');
      }

      if (up_btn == HIGH)      // button '+/up' pressed, show next option
      {
        lcd.home();
        screen4_2(0, 'E');
      }
    }

    if (btn3_state == LOW && subMenu == 1)      // exit options
    {
      lcd.noBlink();
      subMenu = 0;
    }
  break;

  case 2:
	(...)
  break;

  case 6:
    if (up_btn == 1)     // up (5. & 6.)
    {
      clrscr();
      screen4_5(0, 5, 'L');
      screen4_7(1);
      menu = 5;
    }

    if (btn3_prev == HIGH)     // accept and EXIT
    {
      lcd.setCursor(14, 0);
      lcd.write(5);
      start = 0;
      menu = 0;
      btn3_state = LOW;
      btn3_prev = LOW;
    }
  break;

it's unclear what your menu structure is

in the following,

  • it looks like the button press determines what is displayed instead of what the value of menu or subMenu are
  • what are the sub-menus

assuming pressing a button pulls the pin HIGH,

  • looks like both btn3 and down buttons need to be pressed at the same time

checking buttons for each menu/subMenu means the buttons can have different meanings rather than handle buttons in one place, advancing thru menu options and selecting a menu option or changing to a sub-menu

does this help you?
(very simple, not sure what you're trying to do)

// menu

// -----------------------------------------------------------------------------
byte pinsBut [] = { A1, A2, A3 };
#define N_BUT   sizeof(pinsBut)

byte butState [N_BUT];

const int NoBut = -1;

int
chkButtons ()
{
    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        byte but = digitalRead (pinsBut [n]);

        if (butState [n] != but)  {
            butState [n] = but;

            delay (10);     // debounce

            if (LOW == but)
                return n;
        }
    }
    return NoBut;
}

// -----------------------------------------------------------------------------
struct MenuItem {
    int         mode;
    const char *desc;
};

MenuItem menu0 [] {
    { 1, "mode1" },
    { 2, "mode2" },
    { 3, "mode3" },
};

const int MenuSize = sizeof(menu0) / sizeof(MenuItem);
int menuIdx;

int mode;

// -------------------------------------
enum { Bexit, Badv, Bsel };
int  menuFlag;

int
menu (
    int but )
{
    if (menuFlag) {         // ignore button when 1st pressed
        switch (but) {
        case Bexit:
            break;

        case Badv:
            if (MenuSize <= ++menuIdx)
                menuIdx = 0;
            break;

        case Bsel:
            mode = menu0 [menuIdx].mode;
            Serial.print   ("  menu select - ");
            Serial.println (menu0 [menuIdx].desc);
            return 1;
        }
    }

    menuFlag = 1;

    Serial.print   ("  menu - ");
    Serial.println (menu0 [menuIdx].desc);
    return 0;
}

// -----------------------------------------------------------------------------
const unsigned long MsecPeriod = 2000;
      unsigned long msec0;

void
loop ()
{
    unsigned long msec = millis ();
    if (msec > msec0) {
        msec0 += MsecPeriod;

        switch (mode) {
        default:
            Serial.print   ("mode ");
            Serial.println (mode);
            break;
        }
        menuFlag = 0;
    }

    int but = chkButtons ();
    if (NoBut != but)  {
        if (menu (but))
            msec0 = msec;
        else
            msec0 = msec + 5000;
    }
}

// -----------------------------------------------------------------------------
void
setup ()
{
    Serial.begin (9600);

    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        pinMode (pinsBut [n], INPUT_PULLUP);
        butState [n] = digitalRead (pinsBut [n]);
    }
}

What menu is displayed is controlled by value of 'menu' in 'switch..case' statements. This is still work in progress, but works very well so far.

It won't be huge or universal menu system for commercial use. I do NOT intend to give super advanced and flexible menu system here. It's just for my hobby project and to remind myself how to use C++. I'm not IT expert or so.

  • 'up' & 'down' buttons control value of 'menu', hence which menu screen is displayed.
    'subMenu' flags if I entered ('subMenu=1') menu item to scroll through options or not ('subMenu=0'). See my 1st. post (i.e. in '2.Temp.scale' menu item I have '*C', '*F', 'K' options).
  • Pressed push button is HIGH, released is LOW.

'btn3_state' is for push button 'Enter/Exit' and it works as a toggle switch here. If I press 'btn3' it changes from LOW to HIGH (and stays HIGH). Then I can press either 'down_btn' or 'up_btn' to scroll through options in selected menu. Pressing 'btn3' again flips 'btn3_state' from HIGH to LOW (and stays LOW), that will save (not coded yet) selected option and takes me back to scrolling main menu again (see attached picture).

  • '-/down' button is used (as push button) to change set temperature down, scroll menu/screen down, change to previous option of selected menu item.
  • '+/up' button is used (as push button) to change set temperature up, scroll menu/screen up, change to next option of selected menu item.
  • 'Enter/Exit' button is used either as push button or toggle switch. As toggle switch to enter chosen menu item to scroll option and to exit chosen menu item.
  • 'Enter/Exit' button used as push button is used to accept/select '6.Exit' menu item to leave Settings menu and go back to main screen (work in progress).

I'm sure your sketch can help me in the future, but first I have to learn more about C++ to understand what's in it. I will be happy to analyze and play with it. Thank you for your help.

guessing you're not familiar with structures. see chap 6 in The C Programming Language

  • do you really need a '-' button? are there that many choices
  • do you need an exit button to 1) go back to the previous menu or 2) exit from menu mode instead of letting it time out

so it looks like you need sub-menus

Thank you for the link. That book will keep me busy for a while :slightly_smiling_face:

I finished menu today. All options are accessed and modified using 3 push buttons. Maybe it is not most beautiful sketch but works without issues. I'll optimize code in the future, when I learn more about C++.

Thank you for help.

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