Author Topic: Best way to implement an LCD menu!  (Read 1381 times)

0 Members and 1 Guest are viewing this topic.

Offline kgavionicsTopic starter

  • Regular Contributor
  • *
  • Posts: 198
  • Country: ca
Best way to implement an LCD menu!
« on: June 30, 2023, 09:33:11 pm »
First, I want to let you know that I'm not a professional in embedded system, I'm only an enthusiast. I have a project where I need to navigate through 4 menus (one main menu and three sub menus). I have seen a lot of a project online using a tree data structure (which I find very intimidating, because of the extensive use of pointers). My knowledge in C is rather intermediate, and I want to find a good way to implement a simple LCD menu without getting the project too complicated! My question why the majority of the project for Menu are using Tree data structures, and what advantages that the latter has over let's say a simple switch statement?


Thanks in advance
 

Online mariush

  • Super Contributor
  • ***
  • Posts: 5036
  • Country: ro
  • .
Re: Best way to implement an LCD menu!
« Reply #1 on: June 30, 2023, 10:12:41 pm »
It doesn't have to be pointers

If it's just a main menu and 3 sub menus, you could store everything in a string for example

[ 1 byte : number of menu entries and flags (ex  first bit is set, means this menu has submenus, 2nd bit is set this means shows "back to previous menu" option after all options, last 6 bits = number of menu entries]
[ n  x 3 ]  :  [ 1 byte : offset where the text for the menu option starts,  1 byte : length of text, 1 byte : where the submenu definition starts  ]


So for example here's the definition of a menu :

Alpha
 * X
 * Y
Beta
 * 123
 * 567
 * 890
Gamma
 * Test

strings would be  "AlphaBetaGammaXY123567890Test"

Your bytes would be

byte 0 : 13 (3<<2 + 0 + 1)  ( 3 menu entries, don't show back to previous menu because it's root menu, all entries are submenus
byte 1,2,3:  offset = 0, length = 5, jump =11   (Alpha , go to menu definition that starts at byte 11)
byte 4,5,6:  offset = 5, length = 4, jump = 18  ( Beta, go to menu definition that starts at byte )
byte 7,8,10:  offset = 9, length = 5, jump =  ( Gamma , go to menu )
byte 11:  10  ( 2 << 2 + 2 + 0) (2 menu entries, show previous menu after all options,
byte 12,13,14 : offset = 13, length = 1, jump = 0  (X , no submenu)
byte 15,16,17 : offset = 14, length = 1, jump = 0  (Y , no submenu)
byte 18 : 65 (2 << 2  + 1 + 0)

and so on..

A single function can use these two strings to loop through menu options and when a menu option is chosen, it can return parent menu * 256 + submenu option  or something like that.  ex Beta->567 = 2*256 + 2

You can compress it further if you know you're not gonna have more than 16 characters per menu entry (use 4 bits for length of string instead of a full byte). you could inline the strings and then you'd just have to parse the string every time to get to the submenu and the strings
ex 

[13][5]Alpha[4]Beta[5]Gamma[10][1]X[1]Y[14][3]123[3]567[3]890[6][4]Test

« Last Edit: June 30, 2023, 10:19:26 pm by mariush »
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26960
  • Country: nl
    • NCT Developments
Re: Best way to implement an LCD menu!
« Reply #2 on: June 30, 2023, 10:52:29 pm »
Maybe a 2 dimensional array could be a solution. With a struct like
struct TMenu
{
char *name;
int min_val, max_val;
int type;
} TMenu;

A NULL pointer for name means no more menu items. The item with index x, 0 (where x is the main menu index) is the main menu item. Not the most memory efficient way but for a small number of menus this will be very simple to implement.
« Last Edit: July 01, 2023, 12:16:01 am by nctnico »
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Online T3sl4co1l

  • Super Contributor
  • ***
  • Posts: 21718
  • Country: us
  • Expert, Analog Electronics, PCB Layout, EMC
    • Seven Transistor Labs
Re: Best way to implement an LCD menu!
« Reply #3 on: July 01, 2023, 12:18:26 am »
Well, it is pointers, but, if you'd just like an example of how that can be done -- I have a public example here,
https://github.com/T3sl4co1l/Reverb/blob/master/menu.c
(and related files)

This implements a basic 4-button interface, up/down navigates a menu, right/left enters/exits a submenu or activates the item, etc.

The confusing things about constructing this are, I think:
1. How do you pack everything into (an) array(s)?  Does it have to be fixed-length, and, how can I make the compiler satisfied that my types match?!
2. How to interleave functions and data? (Partly semantics, partly documentation/coding style.)
3. How to format the data so it can be navigated?  What does it even mean to navigate a menu?

1. The above example shows a flat array; although, it doesn't have many (...any??) submenus, but, uh, I recall at least that I designed it to handle that just fine?

One catch is, it's entirely in RAM.  I think some values are mutated at runtime so that's kind of justified, but those could be (should, even?) special-cased so that (almost) everything is pulled from Flash instead (which on the AVR, is a special memory space with specifier keywords and special access functions).  As I had plenty of RAM and just wanted to throw together the project, I didn't explore this further.  (On an ARM, say, or other platform with flat memory space including the ROM/Flash, this is a lot easier.)

Array lengths are irrelevant as type is discarded by the void pointer; lengths are not used, but a "sentinel" entry is used to define the bottom of the list.  This is also used as a header, containing information on the active menu itself (e.g. functions to execute on entering, moving, exiting it).  Defaults could also be placed here, like if you want all sub-menus of one group to return to the group root, rather than going back level by level.  (Or going back to main menu, but that would be easier handled as a default, no need to write that pointer into every submenu.)

2. Interleaving is... an open question.

The semantics are: you can't exactly write lambdas into C.  The style is: even if you did [inline functions / lambdas], the state variables those functions operate on might not share locality with where they're defined (i.e., in the menu data structure).  I guess if I were doing it in JS for example, I wouldn't mind putting most functions (the piddly menu housekeeping stuff) inline, and maybe keeping meatier calls in a separate location?  It's going to be a little confusing at times, even in the best of cases; code graciously, and add comments to point to relevant code or objects.  (Remember you're not so much writing for the compiler, as for the next person/people who will read this; and often that person is yourself, however many years later, having long forgotten how you put it all together!)

3. Navigation, I think there are two ways that usually come to mind.

Your, maybe, first thought / a more naive approach? might be:
Okay, I have program execution as state variable, and when I jump into a function, that function is the whole thing that's active at the moment.  It draws its menu items, it accepts input, etc. etc.

Well, first problem with that is -- if you jump endlessly in circles, between functions whose whole purpose is their entire experience (I/O and functionality), you're never returning, and stack overflow quickly ensues.

You can use function return to implement menu return, which is nice if you're doing a strict hierarchical design.  Beware, it could be that, some time you want to implement a sub-menu that links to an otherwise distant sub-menu -- you're approaching some aspect of the system from two different directions, and they share that one menu in common, so, it would make sense to be able to navigate to it from both ways.  Which one do you return to, the previous one, the connected one (which?), or what?  What if it links back, and now you have circles again?  So you could end up with bridges across your tree structure, or whole loops, and now you have graph problems, not just menu problems.

The most common case being a toolbar / heading / table of contents sort of thing.

It would be nice to avoid duplicating all of that function logic.  If the function can be broken down to a skeleton of operations, maybe the same function can be used all the time, and instead of jumping between functions, you stay in the same one but its parameters vary.

But you might need to make those parameters into function calls, because you need real code statements to perform diverse actions with it.

So, the route I took, was to attach those function calls to each menu item that needs them, or defaults for otherwise passive items.  Type checking is disabled by use of a void pointer, and when a menu option is executed, the compiler simply assumes that the location being jumped to, actually has the function prototype specified.

So the,

Code: [Select]
} else if (t == MENU_FUNC) {
if (menuMenu[menuIndex + menuPosY].i.ptr != NULL) {
((void(*)(void))menuMenu[menuIndex + menuPosY].i.ptr)();
}
}

the cryptic little pile of parenthesis is not a lisp, but how you typecast a pointer to a function in C, namely of type void foo(void).  The outer ()() is taking the value of the first parens (the function type) and executing it as a function (the second parens are the parameter list).

And, there are safer ways to use data types here; again, I used voids partly for expedience.  I do recall trying once long ago, and I couldn't convince the compiler that the circle of references was actually reasonable and yes it's okay trust me; I'd probably solve that problem nowadays, but probably still have to settle for type declarations (including fixed length , which makes things less readable and harder to maintain...

As for string methods or whatnot -- that's viable too, I don't like it from a one-off coding perspective but it's certainly doable with more work.

I haven't done that for a menu yet, but it can offer excellent compression, which becomes attractive if you're using a whole lot of them.  I have, however, used similar methods for image compression; consider this:
https://github.com/T3sl4co1l/st7735_gfx
mostly this part,
https://htmlpreview.github.io/?https://github.com/T3sl4co1l/st7735_gfx/blob/master/compr.html

not that it's very functional, but as an example of a sequential byte code sort of approach.  Putting functions into that, or let alone string offsets and other coding features, can quickly develop into a DSP (domain specific programming language) where you've not only not solved the underlying complexity but added a convoluted layer on top of it.  :-DD So, it must be used carefully.  Obviously if we're just talking about bytecode for image compression, or a very basic menu system, that's one thing, but be very careful about feature creep, as this is exactly the kind of point where a codebase can be expanded just about indefinitely over time.  (Not saying "don't".  Just that... there are stories. :-DD Always code judiciously. :) )

Tim
Seven Transistor Labs, LLC
Electronic design, from concept to prototype.
Bringing a project to life?  Send me a message!
 

Offline pqass

  • Frequent Contributor
  • **
  • Posts: 727
  • Country: ca
Re: Best way to implement an LCD menu!
« Reply #4 on: July 01, 2023, 05:46:38 am »
... I want to find a good way to implement a simple LCD menu without getting the project too complicated! My question why the majority of the project for Menu are using Tree data structures, and what advantages that the latter has over let's say a simple switch statement?

A: to separate the navigation of the menu system itself from each menu item's function; ie. to breakdown into manageable pieces.

See the following example data structures that can represent an arbitrary number of nested menus.  There are only two arrays of two structures; one to define an array of menu nodes, another to define an array of stack items to remember which menu+item that the user has traversed.

The menu declarations and each item function (code) are nicely separated and can be easily changed/extended.

The root of the menu starts at "struct node myTopMenu[] = { .... }".  Whenever a key is pressed, "updateMenuState(key)" is to be called to update the current state of menu system.

The code compiles but I haven't tested it. You'll get the gist.  Caveat emptor.
Code: [Select]
struct node {
  char *name;
  void (*func)();
  struct node *subnode;
};

void doItem11Func() { /* code here */ }
void doItem12Func() { /* code here */ }
struct node myItem1Submenu[] = {
  {.name="Item 1.1", .func=doItem11Func, .subnode=NULL },
  {.name="Item 1.2", .func=doItem12Func, .subnode=NULL },
  {.name=NULL,       .func=NULL,         .subnode=NULL }  // end of menu marker
};

void doItem331Func() { /* code here */ }
void doItem332Func() { /* code here */ }
struct node myItem33Submenu[] = {
  {.name="Item 3.3.1", .func=doItem331Func, .subnode=NULL },
  {.name="Item 3.3.2", .func=doItem332Func, .subnode=NULL },
  {.name=NULL,         .func=NULL,          .subnode=NULL }  // end of menu marker
};

void doItem31Func() { /* code here */ }
void doItem32Func() { /* code here */ }
struct node myItem3Submenu[] = {
  {.name="Item 3.1", .func=doItem31Func, .subnode=NULL            },
  {.name="Item 3.2", .func=doItem32Func, .subnode=NULL            },
  {.name="Item 3.3", .func=NULL,         .subnode=myItem33Submenu },
  {.name=NULL,       .func=NULL,         .subnode=NULL            }  // end of menu marker
};

void doItem2Func() { /* code here */ }
void doItem4Func() { /* code here */ }
struct node myTopMenu[] = {
  {.name="Item 1", .func=NULL,        .subnode=myItem1Submenu },
  {.name="Item 2", .func=doItem2Func, .subnode=NULL           },
  {.name="Item 3", .func=NULL,        .subnode=myItem3Submenu },
  {.name="Item 4", .func=doItem4Func, .subnode=NULL           },
  {.name=NULL,     .func=NULL,        .subnode=NULL           }  // end of menu marker
};

struct menuStackItem {
  struct node *curMenu;
  uint8_t      curItem;  // last item of curMenu descended into
};

#define STACKSIZE 4
uint8_t myMenuStackIndex = 0;                      // which stack element (menu) is in effect
struct menuStackItem myMenuStack[STACKSIZE] = {    // menus don't go beyond this many deep
  { .curMenu=myTopMenu, .curItem=0 },
  { .curMenu=NULL,      .curItem=0 },
  { .curMenu=NULL,      .curItem=0 },
  { .curMenu=NULL,      .curItem=0 }
};

void lcdPrint(char *) { /* code here */ }

void updateMenuState(char key) {
  struct menuStackItem *curMenu = &myMenuStack[myMenuStackIndex];
  switch(key) {
    case 'u':
    case 'U':  //up
      if (curMenu->curItem > 0) curMenu->curItem--;
      lcdPrint(curMenu->curMenu[curMenu->curItem].name);
      break;
    case 'd':
    case 'D':  //down
      if (curMenu->curMenu[curMenu->curItem+1].name != NULL) curMenu->curItem++;
      lcdPrint(curMenu->curMenu[curMenu->curItem].name);
      break;
    case 'i':
    case 'I':  //step in
      if (curMenu->curMenu[curMenu->curItem].func != NULL) {    // exec func if it exists
        (*curMenu->curMenu[curMenu->curItem].func)();
      }     
      if (curMenu->curMenu[curMenu->curItem].subnode != NULL) { // step into submenu if it exists
        myMenuStackIndex++;                                     // initialize next stack element
        myMenuStack[myMenuStackIndex].curMenu = curMenu->curMenu[curMenu->curItem].subnode;
        myMenuStack[myMenuStackIndex].curItem = 0;              // initialize first item as first one to be seen
      }
      curMenu = &myMenuStack[myMenuStackIndex];   // update new idea of curMenu
      lcdPrint(curMenu->curMenu[curMenu->curItem].name);
      break;
    case 'b':
    case 'B':  //step back
      if (myMenuStackIndex > 0) {
        curMenu->curMenu = NULL;    // not really necessary
        curMenu->curItem = 0;       // not really necessary
        myMenuStackIndex--;
      }
      curMenu = &myMenuStack[myMenuStackIndex];   // update new idea of curMenu
      lcdPrint(curMenu->curMenu[curMenu->curItem].name);
      break;
    default:
      break;
  }
}
« Last Edit: July 01, 2023, 05:48:17 am by pqass »
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 8183
  • Country: fi
Re: Best way to implement an LCD menu!
« Reply #5 on: July 01, 2023, 05:56:47 am »
Don't overthink it. Implement it the way that feels natural to you first, maybe this is a bunch of ad-hoc printf() calls and switch-cases all over large functions, that's how I implemented quite large menu systems in the past. While you learn, you can adapt to fancier software design patterns, trying to generalize the problems, replace repeating code by repeating data and single code which interprets that data - and add abstraction - but quite (un)surprisingly, that does not always make the code any more maintainable than your first spaghetti solution.
 
The following users thanked this post: ajb

Offline kgavionicsTopic starter

  • Regular Contributor
  • *
  • Posts: 198
  • Country: ca
Re: Best way to implement an LCD menu!
« Reply #6 on: July 01, 2023, 11:00:49 am »
Thank you guys for all your inputs! I’ll try a few of your suggestions!
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 3707
  • Country: gb
  • Doing electronics since the 1960s...
Re: Best way to implement an LCD menu!
« Reply #7 on: July 01, 2023, 02:00:17 pm »
I avoid pointers in C too. In the hands of "less than top experts" they are the source of the vast majority of bugs, and lots of people quite enjoy using them to show how super clever they are and then nobody can maintain the stuff :) So I use arrays.

A lot depends on whether you are able to redraw the entire text screen on each user operation. If yes, then it is easy. You just need to implement a "clear screen" command with cursor(0,0), CR, LF. I am sure in your case this is fine, unless you are running via SPI with a 50Hz SPI clock ;)

I've just done a menu driven factory test feature for my product. Each menu item takes one to a function which implements that item, and when that function returns, the menu is redrawn. That is fine for a single level menu.

Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6274
  • Country: fi
    • My home page and email address
Re: Best way to implement an LCD menu!
« Reply #8 on: July 01, 2023, 02:49:53 pm »
The most important suggestion I can make, is for you to draw mock-ups of the menus first.
It does not matter much whether you draw them by hand on post-it notes, or use some application for it.

I often recommend using your browser, i.e. making HTML+CSS+Javascript mockups of user interfaces, because that way you can make them interactive.  If you end up liking that sort of stuff, you can then just send the file to a customer or test user, and get feedback.  (You can make them fully standalone, too; just check out my finite impulse response filter analysis with e.g. FIR coefficients -1 2 -1, it is all contained in the single 372 line file.)

The implementation does not matter much, if you are not running out of resources on your device.  Menus do not tend to require much computing, just memory, so any kind of speed or efficiency arguments are mostly a waste of time.  What matters, is its maintainability, so do comment your intent as well as you can.  (Describing what the code does is not useful, because we can read that from the code.  But knowing what the code is trying to do, is required for fixing bugs and changing or extending behaviour later on.)

Me, I do like to use pointers, even function pointers, and more magical stuff like collecting individually defined elements into a single continuous array using section attributes (with GCC and Clang compilers).  I don't do it because I can; I only use the forms that actually help with maintenance, and make the code clearer.

I also like to draw diagrams of how the menu behaves, in Inkscape.  For example, let's say you have a power supply with three menu buttons – Left, Right, and Fine – and either two buttons or a rotary encoder to increase or decrease the value.  (The Fine button could be the press button on the EC11-style encoder.)

The gray boxes with arrows indicate buttons or events that change to a different display/menu section.  There are six different menu views: Coarse current adjustment, Fine current adjustment, Coarse voltage adjustment, Fine voltage adjustment, Coarse output display, and Fine output display.  Fine toggles always between fine and coarse, and left and right rotate through output, current, and voltage.

You could also make this a simple 2×3 array:
    Coarse Voltage Coarse Output Coarse Current
     Fine Voltage Fine Output Fine Current
with Fine changing the row, and Left and Right changing the column (with wraparound, i.e. Right at the right edge moves to the leftmost column, and Left at the leftmost column moves to the rightmost column).

This can be implemented just fine with a switch statement, or with the other stuff I mentioned above, it does not matter much.  What does matter, is the documentation and logic.
 

Online DavidAlfa

  • Super Contributor
  • ***
  • Posts: 5930
  • Country: es
Re: Best way to implement an LCD menu!
« Reply #9 on: July 07, 2023, 05:53:49 pm »
Arduino but easily ported to any C system. Pretty simple!



https://github.com/shuzonudas/monoview/blob/master/U8g2/Examples/Menu/simpleMenu/
Hantek DSO2x1x            Drive        FAQ          DON'T BUY HANTEK! (Aka HALF-MADE)
Stm32 Soldering FW      Forum      Github      Donate
 
The following users thanked this post: kgavionics

Offline liaifat85

  • Regular Contributor
  • *
  • !
  • Posts: 172
  • Country: bd
Re: Best way to implement an LCD menu!
« Reply #10 on: July 09, 2023, 07:35:02 am »
I will suggest an Arduino and a TFT LCD. Once upon a time, I used a 3.2 Inch 320×240 Touch LCD (C) Shield with an Arduino Mega for a similar purpose.
 
The following users thanked this post: kgavionics


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf