Categories
Arduino LCD display software

A Menu System for LCD Modules

Menu managers in embedded systems are generally not the main function of the embedded application software, but they can take considerable code and testing to implement.

This article describes a menu library that minimizes the use of RAM and has a small memory footprint overall, leaving more space for what really matters in application code.

I often need to implement menu management as a user interface end to set parameters in my Arduino control applications whilst the background application still runs. I also extensively use text based LCD modules with 1 or 2 display lines for control status information. Most of the existing libraries have too many ‘bells and whistles’ for this situation, so I decided to create my own ‘yet another menu system’ library that simplifies what I need.

The design parameters I set for this project were

  • Static menu definitions in PROGMEM to minimized RAM footprint.
  • Callbacks for navigation and display control to allow flexibility in user interface hardware implementation.
  • Menu inactivity timeout.
  • Auto start on key press or manual start by user code.
  • Input methods available for
    • Boolean (Y/N) values.
    • Pick List selection.
    • Signed integers of all sizes.
    • Decimal floating point representation.
    • Engineering units.

The library described here is available from my code repository.

What is a Menu?

A menu is a list of options from which the user makes a selection. Each option is a brief description for a selection (menu label) that can lead to a sub-menu or a specific action being performed.

The tree data structure is one of the most obvious structures used to represent menus. In trees information naturally forms a hierarchy – menu entries are naturally ordered above or below other entries. The tree data structure is a very efficient way to represent this type of information. Although a tree has built-in hierarchical structure, it can be stored as arrays of data.

Tree Structure Primer

This structure illustrated below is named a tree structure because the classic representation resembles a tree, even though the chart is generally upside down compared to an actual tree, with the root at the top and the leaves at the bottom. The tree elements are called nodes. The lines connecting nodes are called branches. Nodes with no sub-nodes are called leaf nodes, end nodes, terminal nodes or leaves.

Simple Tree Structure

A tree is a special kind of graph that follows a particular set of rules
and definitions:

  • Connected – A graph can be a tree if it is connected. Each node is
    connected with a link to at least one other node.
  • Acyclic – A graph can be a tree if is acyclic (ie, it has no cycles or loops in it). That means there’s only one route from any node to any other code. This also means that a node may have many children but only one parent.
  • Root – The term root commonly refers to a top-most node (ie, the one with no parent). In the figure above, node F is the root of the tree. The root is the starting for traversing any tree structure.
  • Descendant – A descendant is a node that is farther away from the root than some other node. The term descendant is always referenced from another node. In the example, nodes I and H are descendants of node G.
  • Parent – Parent is considered the node that is closer to the root node by one link or vertex, in the same branch. In the figure, node B is the parent of nodes A and D. The ultimate parent in a tree is the root node.
  • Sibling – Sibling (brother or sister) nodes share the same parent. In the example, nodes A and D are siblings.
  • Ancestor – An ancestor is any node between a given node and the root, including the root. In the figure, the ancestors of node H are nodes I, G, and F.
  • Leaf or Terminal Node – A node is terminal if it has no children. In the example, nodes C, E and F are leaves.
  • Height or Depth – The height of a tree is defined as the number of vertices traversed to get to the most distant node. In the figure, the height of the tree is three.

Menu System Data Structures

In a menu system the root node is the start of the menu system and every other node is a child node that is:

  1. A submenu if it has child nodes. In this case it is conceptually the root node of the submenu.
  2. A selection/input item if it is a leaf node.

The limited amount of RAM available in micro controllers is a challenge for menu systems, as they often contain large amounts of ‘static’ data as text labels and other status information.

The MD_Menu library uses statically allocated data located in read-only memory memory (PROGMEM or static RAM) for the menu system and only copies the current menu record into dynamic RAM, as needed. All user values reside in user code and are not duplicated by the library.

As we want to menu structure to be stored statically, the entire menu must be defined at compile time. Arrays are the most convenient way to do this, by defining three types of nodes – menu headers (root node definition), menu items (child node definition) and input items (leaf nodes).

Menu headers define the root node of each submenu, including the root node. Each menu header is identified by a unique identifier and contains the start and end identifiers of the child menu item nodes (submenu). The menu header is used as the label for the submenu being processed.

Menu System Structures Relationship

The menu items array provides the labels for the submenu choices, with each node also identified by a unique number. Within each item an action type can lead to either a new menu header (ie, root of a submenu) or a menu input (ie, a leaf in the tree). If the action is a new menu header, the process repeats with the identified menu header.

When the action is a menu input, the menu input array is referenced for information on how to handle the required input, described below.

The library examples show these structures being used to define different types of menu varied items with attributes and outcomes.

Handling Menu Input

Menu input items define the type of value that is to be edited by the user and parameters associated with managing the input for that value. Before the value is edited the user callback is invoked to ‘get’ the pointer to the variable with the current value. The input item id is provided to identify which value is being requested. The data must be loaded into a standardized data structure that remains in scope while the data is being edited, as the library uses the pointer to the data.

This copy of the user variable is used during editing and a second callback is invoked to ‘set’ the value after it is updated, enabling the user code to take action on the change. If the variable edit is cancelled, the second ‘set’ callback is skipped and no further action is required from the user code.

Additionally, Input values may be specified as external input, causing the ‘get’ callback to be executed repeatedly to obtain values, or with real-time feedback, causing the ‘set’ callback to be executed for each value change.

Variable data input may be of the following types:

  • Pick List specifies a PROGMEM character string with list items separated by the ‘|’ character, for example “Apple|Orange|Pear”. In this case the value is identified as the zero based index of the current selection (‘Pear’ is index 2).
  • Boolean for boolean (Y/N) values. As the user edits the valueit changes between displays of ‘Y’ and ‘N’. Internally the value is identified as a 0/1 for false/true.
  • Integer values can 8, 16 or 32 bits in size. Fo all these values, the get/set callback always deals in 32 bit signed integers. The input item specification allows a lower and upper bound to be set, as well as the number’s base (2 through 16) to be specified. Numeric values that overflow the specified field with are prefixed by the ‘#’ indicator.
  • Floating point values are really 32 bit long integers with an implicit 2 decimal digits precision (which can be changed through a library constant). The input specification allows lower and upper bound to be set and definition of the minimum increment or decrement of the fractional component.
  • Engineering Units are 32 bit long integer and that assume the last 3 digits to be the fraction after the decimal point. These values are displayed using engineering notation as powers of 103. The type of engineering units (eg, Hz, Pa) are defined in the specification and the engineering profiex (eg, m, M, k) are inserted by the library. As with simple floating point values, the input specification allows definition of the minimum increment or decrement of the fractional component.
  • Run Code specifies input fields that are designed to execute a user function when selected. The ‘get’ callback determines whether the operation requires confirmation before action.
  • External Input specifies that the input value is provided by external user code. The ‘get’ callback is invoked until the value is confirmed using the normal method implemented for the menu. All values are 32 bit signed integers.

Menu Display

Display hardware must be able to display one or two lines for the menu display. All menu screens are structured with the first line as title and the second as the current menu selection or currently edited value, as appropriate. If the display can only support one line, actions for the first line should be ignored and only the second line displayed.

Menu display is enabled by user code as a callback routine from the library. An example to work with a 2 line LCD shield is shown below.

static LiquidCrystal lcd(LCD_RS, LCD_ENA, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
bool display(MD_Menu::userDisplayAction_t action, char *msg)
{
  static char szLine[LCD_COLS + 1] = { '\0' };
  switch (action)
    {
    case MD_Menu::DISP_INIT:
      lcd.begin(LCD_COLS, LCD_ROWS);
      lcd.clear();
      lcd.noCursor();
      memset(szLine, ' ', LCD_COLS);
      break;
  case MD_Menu::DISP_CLEAR:
      lcd.clear();
      break;
  case MD_Menu::DISP_L0:
      lcd.setCursor(0, 0);
      lcd.print(szLine);
      lcd.setCursor(0, 0);
      lcd.print(msg);
      break;
  case MD_Menu::DISP_L1:
      lcd.setCursor(0, 1);
      lcd.print(szLine);
      lcd.setCursor(0, 1);
      lcd.print(msg);
      break;
  }
  return(true);
}

The callback is provided with a request of type userDisplayAction_t and a message to display. The library will manage the sequencing of these calls and the callback only needs to implement simple atomic actions.

A variety of display hardware setups are demonstrated in the Test example code provided with the library examples.

Menu Input

Menu navigation is carried out under the control of user code invoked as
a callback routine. The code must comply with a specific function prototype, shown below for a 3 switch (up/down/select) implementation.

#include <MD_UISwitch.h>
const uint8_t INC_PIN = 3;
const uint8_t DEC_PIN = 4;
const uint8_t CTL_PIN = 5;
uint8_t pins[] = { INC_PIN, DEC_PIN, CTL_PIN };
MD_UISwitch_Digital swNav(pins, ARRAY_SIZE(pins), LOW);
void setupNav(void)
{
  swNav.begin();
  swNav.enableRepeat(false);
}
MD_Menu::userNavAction_t navigation(uint16_t &incDelta)
{
  MD_Menu::userNavAction_t nav = MD_Menu::NAV_NULL;
  switch (swNav.read())
  {
    case MD_UISwitch::KEY_PRESS:
    {
      Serial.print(swNav.getKey());
      switch (swNav.getKey())
      {
      case INC_PIN: nav = MD_Menu::NAV_INC; break;
      case DEC_PIN: nav = MD_Menu::NAV_DEC; break;
      case CTL_PIN: nav = MD_Menu::NAV_SEL; break;
      }
    }
    break;
    case MD_UISwitch::KEY_LONGPRESS:
    {
      if (swNav.getKey() == 2)
        nav = MD_Menu::NAV_ESC;
    }
    break;
  }
  incDelta = 1;
  
  return(nav);
}
#endif

This callback routine implementation is dependent on the type of input hardware, but the return codes for the required actions must be one of the standardized userNavAction_t enumerated type. Almost any hardware can be accomodated under application control, as the library is only looking for the navigation return values:

  • INCREMENT (NAV_INC). Move down a menu, move to the next value in a pick list or increment a numeric value being edited.
  • DECREMENT (NAV_DEC). Move up a menu, move to the previous value in a pick list or decrement a numeric value being edited.
  • SELECT (NAV_SEL). Select the current menu or pick list item, or confirm an edited numeric value.
  • ESCAPE (NAV_ESC). Escape the current menu (back up one level) or cancel changes to an edited value.

A variety of input hardware setups are demonstrated in the library example code provided.

The User Experience

The aim is for a consistent user interface. A menu has the title on the top line and the submenu selections appear on the second line, enclosed by angled brackets.

An input item has a label on the top line with the input value edit prompt and field on the second line. The input value is enclosed by square brackets.

User navigation is implemented by the application but should also provide a consistent experience. In the library example program siwtching is implemented using the MD_UISwitch library and:

  • INC and DEC each have one switch or they are the rotation clicks of a rotary encoder.
  • One switch is shared for for SEL (click) or ESC (long press). For a rotary encoder this is the built in ‘knob’ switch.

The MD_Menu library is not for every application, but it comes into its own when a flexible but low impact (memory and CPU) solution is needed to implement a simple user interface that keeps out of the way of the main application code.

2 replies on “A Menu System for LCD Modules”

Thank you Marko you remind me of this very clever library. I am wondering if the 2 lines is standard or is there any variable to make it for 4 lines LCD?

We are going to embed it on our m10cube project as standard M10CUBE project: https://hackaday.io/project/171770-m10cube GitLAB : https://gitlab.com/m10cube/m10

Any suggestions for OLED displays?

Thanks again for your very scientific material you provide us

Take care Regards

Vasilis Vorrias

Electronics and Automation
M10CUBE project: https://hackaday.io/project/171770-m10cube GitLAB : https://gitlab.com/m10cube/m10
Against GM food

Like

The library is designed or 2 line displays but it will work with just one line. To change it to use 4 lines requires significant rework to the logic and I would suggest a different library may be more suitable. However, on a 4 lines display any 2 lines can be used (eg, lines 1/2, 2/3 or 3/4) as this is under the control of the programmer in the ‘display’ call back shown in the article.

Regarding OLED, the output device can be anything as long as the display can be initialized and the 2 lines used for the display cleared and text lines printed. The fonts and spacing may impact how the display looks but I don’t have any experience trying that. The example code can use alterative displays based on Serial output and LED matrix modules/MD_Parola. See the Menu_Test_Display.cpp file.

Edit: With the latest update (v2.1.3) I have also included an example of an OLED display.

Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s