Best way to implement an LCD menu! - Page 1
Forum section: electronics / general

author: kgavionics
author_rank: Regular Contributor
timestamp: June 30, 2023, 09:33:11 pm
content: 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

author: mariush
author_rank: Super Contributor
timestamp: June 30, 2023, 10:12:41 pm
content: It doesn't have to be pointersIf 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* YBeta* 123* 567* 890Gamma* Teststrings would be "AlphaBetaGammaXY123567890Test"Your bytes would bebyte 0 : 13 (3<<2 + 0 + 1) ( 3 menu entries, don't show back to previous menu because it's root menu, all entries are submenusbyte 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 + 2You 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 stringsexAlphaBetaGammaXY123567890Test

author: nctnico
author_rank: Super Contributor
timestamp: June 30, 2023, 10:52:29 pm
content: Maybe a 2 dimensional array could be a solution. With a struct likestruct 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.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: July 01, 2023, 12:18:26 am
content: Well, itispointers, 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: youcan'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 havegraph 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:} 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 typevoid 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 andyes 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_gfxmostly this part,https://htmlpreview.github.io/?https://github.com/T3sl4co1l/st7735_gfx/blob/master/compr.htmlnot that it's veryfunctional, 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.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.Always code judiciously.)Tim

author: pqass
author_rank: Super Contributor
timestamp: July 01, 2023, 05:46:38 am
content: Quote from: kgavionics on June 30, 2023, 09:33:11 pmA: 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: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 4uint8_t myMenuStackIndex = 0; // which stack element (menu) is in effectstruct menuStackItem myMenuStack = { // 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;switch(key) {case 'u':case 'U': //upif (curMenu->curItem > 0) curMenu->curItem--;lcdPrint(curMenu->curMenu[curMenu->curItem].name);break;case 'd':case 'D': //downif (curMenu->curMenu[curMenu->curItem+1].name != NULL) curMenu->curItem++;lcdPrint(curMenu->curMenu[curMenu->curItem].name);break;case 'i':case 'I': //step inif (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 existsmyMenuStackIndex++; // initialize next stack elementmyMenuStack.curMenu = curMenu->curMenu[curMenu->curItem].subnode;myMenuStack.curItem = 0; // initialize first item as first one to be seen}curMenu = &myMenuStack; // update new idea of curMenulcdPrint(curMenu->curMenu[curMenu->curItem].name);break;case 'b':case 'B': //step backif (myMenuStackIndex > 0) {curMenu->curMenu = NULL; // not really necessarycurMenu->curItem = 0; // not really necessarymyMenuStackIndex--;}curMenu = &myMenuStack; // update new idea of curMenulcdPrint(curMenu->curMenu[curMenu->curItem].name);break;default:break;}}

author: Siwastaja
author_rank: Super Contributor
timestamp: July 01, 2023, 05:56:47 am
content: 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 repeatingdataand 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.

author: kgavionics
author_rank: Regular Contributor
timestamp: July 01, 2023, 11:00:49 am
content: Thank you guys for all your inputs! I’ll try a few of your suggestions!

author: peter-h
author_rank: Super Contributor
timestamp: July 01, 2023, 02:00:17 pm
content: 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 stuffSo 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 clockI'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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 01, 2023, 02:49:53 pm
content: 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 myfinite impulse response filter analysiswith 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 usingsectionattributes (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 VoltageCoarse OutputCoarse CurrentFine VoltageFine OutputFine Currentwith 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.

author: DavidAlfa
author_rank: Super Contributor
timestamp: July 07, 2023, 05:53:49 pm
content: Arduino but easily ported to any C system. Pretty simple!https://github.com/shuzonudas/monoview/blob/master/U8g2/Examples/Menu/simpleMenu/

author: liaifat85
author_rank: Regular Contributor
timestamp: July 09, 2023, 07:35:02 am
content: 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.