Projects Research & Design Hub

Hierarchical Menus for Touchscreens

Written by Aubrey Kagan

Crafting the Code

In his December article, Aubrey discussed his efforts to build a display subsystem and GUI for embedded use based on a Noritake touchscreen display. This time he shares how he created a menu system within the constraints of the Noritake graphical display system. He also explains how he made good use of Microsoft Excel worksheets as a tool for developing the menu system.

When it comes to custom user interfaces, experience has taught me that no two people will take the same approach. But even more problematic is that often the customer doesn’t share the same vision as the programmer and will want changes. And then more change. Followed by some tweaks. Followed by revision 2. As I mentioned in my previous article (December 2018, Circuit Cellar 341), it was my intention to generate an approach that allows me to create and change displays with minimum effort, and preferably no additional programming. I already had an approach for monochrome alphanumeric displays [1] but I wanted to adapt and improve to allow for more modern graphical displays along with touch keys.

To recap, I’m using the Noritake GT800X480A-C903PA LCD color display (7″ diagonal). I am driving it through an RS- 232 serial port of a Cypress Semiconductor PSoC5LP microcontroller (MCU). The PSoC 5 includes an Arm M3 processor and its raison d’être is that the internal arrays can be set up with many different peripherals. However, for this project the only point of note is that the serial port can be configured with a transmit buffer which I have set to 2,000 bytes.

I’m not using a real-time operating system (RTOS), but instead work with what is called “cooperative multitasking.” I did try to explain my implementation in my December article. I’ve implemented it in plain C language and I think it could be ported to almost any MCU with very little change. The description of the basic operation is included in the previous article. On Circuit Cellar’s article code download page, you can download the software for the display as a single file (MainDisplay2.c) and some of the public variables that are declared in a header file (memory2.h). As you’ll see later, one of my screens will carry a log of events. Since this is early days in my project, I created a simple event generator (process.c) to provide a little guidance on the use of a semaphore to update the events on the screen. There are bugs with the pointers of the ring buffer, I know. But correcting them is beyond the scope of what I’m trying to achieve for this article.

PHILOSOPHY
My user interface is based around screens. User interaction and system events allow transition between screens. Although the Noritake display is versatile in its implementation of the touch control, I have chosen only one approach with 10 × 6 keys and it is permanently enabled. My software will allow for interpretation of the key touches. Each screen has a number of elements: several lines of text, outlines and button captions indicating which button functions are available and 0-4 windows. It is my intention that dynamic changes of the system are displayed inside the windows on the screen. The screen itself should be static—the text being used for titles and other presentation effects. Just to whet your appetite, here’s a brief description of my start-up screen (Figure 1).

FIGURE 1 – This is screen 0. Two variations of the startup screen. The graphic logo, faintly visible at the lower left has been obliterated to protect the innocent. If aesthetics is something that can be learned, obviously I have not acquired the knowledge. Tapping anywhere outside the event window, will take you to the main menu of Figure 2. Tapping within the event window will toggle between screen A and screen B.

The two lines of screen text are the title “MAIN SCREEN” and the “STATUS” text indicating the contents of window below it. The logo is in a window, and since I have not implemented a frame around the window, this may not be obvious. The date and time are displayed in the second of the three windows and is updated every minute. The third window contains the system status as it changes. There are two buttons (marked “NEWER” and “OLDER”) each made up of two of the underlying keys. Perhaps intuitively, there is further control by touching the screen in different locations to trigger different responses, which I will get into later. That description will also cover the “flick” action.

— ADVERTISMENT—

Advertise Here

Aesthetically speaking, perhaps there should be no window frame around the time. That’s easy to change by altering one byte in the constants used to configure the window. If you are working with my code, obviously there will be no graphic initially stored in your display’s FROM memory and a description of how to get an image is described in my December article. Figure 1a shows the screen with only the latest event shown.

When there is an alarm, the first line will turn red. By touching the status window, the system toggles to a verbose mode as seen in Figure 1b. The events scroll down as they occur. If you want to scroll up (NEWER) or down (OLDER) the contents of the event buffer are “frozen”—as indicated by the text turning from white to blue. The scrolling can also be induced by a flick up or down anywhere on the screen.

SIMPLIFICATIONS
Rather than use the full capabilities of the display, I have used a simplified approach. The touchscreen is always on and set to a 10 x 6 layout. Almost every item on the screen is aligned with that underlying keyboard. I only apply the color/background color/text size to a single line, disallowing different character colors in a line of text. I only use one font (6×8) and any magnification is “square”—that is the same in the X and Y directions (I have discovered I need some minor exceptions). Oh, and I have appropriated the character “@” for my own nefarious purposes.

With a hierarchy of menus, it isn’t difficult to know where you are going when ascending the menu ladder. The problem is: What happens when you want to go back? I have implemented a stack arrangement. When a new screen is called, the originating screen number (screens are numbered from 0) is pushed onto the stack (variable array cScreenStack). Popping the screen would return you to the calling screen. Simple, right? But wait a second. Sometimes you want to go back more than one screen—for instance, the sequence to set a parameter may go up two screens list of parameters —› current parameter value —› modify value. Once modified it would be elegant to go directly back to list of parameters. For each screen we know how many levels we would like to pop and this is stored in the ScreenType structure in a parallel array cScreenPopLevel (see the constant cPopStackLevel described below).

Any text that appears on any screen is defined in an array Messages. They are grouped so that it’s easy to change to another language based on one of the dimensions of the array. There may be a code embedded (prefaced by the “@” symbol) into the text that denotes where the result of a calculation or some variable is to be placed. For instance, one of the lines of the array reads: {“MAIN SCREEN @010000”},//1. The “@010000” directs that the actual version number will be written to the location occupied by the “@” and the subsequent 6 digits direct that the word (that’s 2 bytes) stored in a particular memory location is to be formatted and written to the screen as you see at the top left of the screen in Figure 1a as “0V00” in the “MAIN SCREEN” line. This approach provides versatility in dealing with system parameters. That deserves a more detailed explanation, so let’s defer that for now.

STRUCTURES
In order to allow the maximum flexibility, almost every object is defined by a group of parameters encapsulated into structures. Adding some confusion is that some structures will point to parameters within other structures. As the project progressed, I started to question my sanity. But now that it’s done, I’m hoping that adding and changing features will be significantly easier than coding/re-coding each screen. Because of the complexity, let me apologize up front that my descriptions may be heavy going.

The fundamental structure is the ScreenType (Listing 1). It is instantiated as an array, Screen[], so that each instance of the array is a new screen. It defines the contents of the four possible windows (including unimplemented windows) (cWindow[]) assigning a number that will be used in a case statement in the program. It is important to understand the difference between a screen and a window. A window is an element contained in a screen. The constant cPopStackLevel is discussed earlier in the article. The keyboard selection (cKeyboardSelection) is used in the software as a pointer to the instantiate structure Keyboard. It was my original intention that there would be a similar pointer to the Text structure, but things got away from me and the text selection for each window is standalone, linked only by the window number. My bad.

struct ScreenType {
//a screen is made up as follows
uint8 cWindow[MAX_NO_OF_WINDOWS];//4 windows- pointer to the contents of each window
uint8 cPopStackLevel; //sometimes (like password) need to go back more than one level
uint8 cKeyboardSelection; // which keyboard,
uint8 cKeyboardVector;
//it possible to have a different response since a keyboard can be contextual
};

LISTING 1 – The structure ScreenType is instantiated as an array, Screen[], so that each instance of the array is a new screen. It defines the contents of the four possible windows assigning a number that will be used in a case statement in the program.

— ADVERTISMENT—

Advertise Here

Each screen can have certain groupings of text that are defined in the TextSetup structure (Listing 2). It is instantiated as an array (Text[]), so that each element of the array pertains to a specific screen. The parameters determine the number of lines in a particular screen grouping (cNumberOfLines), the coordinates of the start of each line (iCursorX and iCursorY), the background color of the text of each line (cBackgroundColour), the color of the text of each line (cTextColour), the text magnification of each line (cTextSize) and the array address of each line (cText) in Messages [] of each line used. In one of my simplifying choices for the display, I opted that the colors be determined by three 8-bit numbers that control the red, green and blue intensity of the LEDs. All three must be provided when specifying a color and you can see that reflected by the additional dimension in all the color constants.

struct TextSetup {
uint8 cNumberOfLines; //on main display- limit to MAX_LINES_ON_MAIN_DISPLAY
uint16 iCursorX[MAX_LINES_ON_MAIN_DISPLAY];//x of starting cursor position
uint16 iCursorY[MAX_LINES_ON_MAIN_DISPLAY];//y of starting cursor position
uint8 cBackgroundColour[MAX_LINES_ON_MAIN_DISPLAY][3];//background colour of the text
uint8 cTextColour[MAX_LINES_ON_MAIN_DISPLAY][3];//color of the actual text
uint8 cTextSize[MAX_LINES_ON_MAIN_DISPLAY]; //size of text using FONT MAGNIFICATION
uint8 cText[MAX_LINES_ON_MAIN_DISPLAY];//actual message number
};

LISTING 2 – Each screen can have certain groupings of text that are defined in the TextSetup structure. It is instantiated as an array Text[], so that each element of the array pertains to a specific screen.

A keyboard arrangement is defined in the KeyboardType structure instantiated as an array Keyboard (Listing 3). Each element of the array represents a different keyboard. The same keyboard could be used on different screens. The constant cNumberOfKeys defines the number of visible keys on the keyboard. Since all keys are implemented, “visible” indicates that there is an outline of the button shown on the screen. The constants include the top left (iTopLeftCoordinateX and iTopLeftCoordinateY) and bottom right (iBottomRightX and iBottomRightY) coordinates of the outline.

struct KeyboardType {
uint8 cNumberOfKeys; //the number of keys of the particular keyboard
uint16 iTopLeftCoordinateX[MAX_NUMBER_OF_KEYS]; //top left coordinate, x
uint16 iTopLeftCoordinateY[MAX_NUMBER_OF_KEYS]; //top left coordinate, y
uint16 iBottomRightX[MAX_NUMBER_OF_KEYS]; //size in pixels, x
uint16 iBottomRightY[MAX_NUMBER_OF_KEYS]; //size in pixels, y
uint8 cKeyColour[MAX_NUMBER_OF_KEYS][3];
uint8 cTextSize[MAX_NUMBER_OF_KEYS];
uint16 iCaptionPointer[MAX_NUMBER_OF_KEYS];
};

LISTING 3 – A keyboard arrangement is defined in the KeyboardType structure instantiated as an array Keyboard. Each element of the array represents a different keyboard; the same keyboard could be used on different screens.

Note that this outline specification could cover more than one button, and needn’t be square. The color of the button outline is defined with the 3-byte array cKeyColour along with the caption text size cTextSize. The actual button caption—fixed in iCaptionPointer—is a pointer to the entry in the Messages array. The caption is placed in the center of the button. In my development, it seemed that the processor was fast enough that there was no perceptible delay in the response to a screen touch. Since I also intend to generate an audio signal with each touch, I decided not to implement a visual indication—like changing the button fill—that the button has been touched.

The structure and contents of each window are defined in the WindowType structure instantiated as Window[] (Listing 4). All four windows in a screen are defined in the structure so that each instance of the Window array is associated with a screen. The elements of the Window configuration include whether there is a frame around each of the windows (cFrame), the color of each of the frames (cFrameColour) and the text color (cFrameTextColour) used in each of the frames. The top left coordinates of each of the frames are provided (iTopLeftCoordinateX and iTopLeftCoordinateY) and size (in pixels) in the X and Y direction (iWindowSizeX and iWindowSizeY). The size is used instead of the lower right coordinates as a result of an inconsistency in the GT800’s commands between creating a window and drawing a box.

struct WindowType {
//a screen is made up as follows
uint8 cFrame[MAX_NO_OF_WINDOWS];// frame around window, 0 no frame 1 frame present
uint8 cFrameColour[MAX_NO_OF_WINDOWS][3];
uint8 cFrameTextColour[MAX_NO_OF_WINDOWS][3];
uint16 iTopLeftCoordinateX[MAX_NO_OF_WINDOWS]; //top left coordinate, x
uint16 iTopLeftCoordinateY[MAX_NO_OF_WINDOWS]; //top left coordinate, y
uint16 iWindowSizeX[MAX_NO_OF_WINDOWS]; //size in pixels, x
uint16 iWindowSizeY[MAX_NO_OF_WINDOWS]; //size in pixels, y
uint8 cWindowProcess[MAX_NO_OF_WINDOWS];
//a number detemining wich windo process to execute i.e. what to load into each window
};y

LISTING 4 – The structure and contents of each window are defined in the WindowType structure instantiated as Window[]. All four windows in a screen are defined in the structure so that each instance of the Window array is associated with a screen.

SEQUENCE OF EVENTS
Incoming serial data is received and processed in an interrupt routine that parses the message and provides 2 bytes that identify which button has been pressed. Once stored, a flag it raised to indicate reception (bit SCREEN_TOUCHED in the iStatus register).

Just prior to the execution of the state machine that controls the display (UpdateMainDisplay) there is a short process that determines if a flick has been received based on the number of keys measured in a short period. If a flick is detected, it is flagged by the bit SCREEN_TOUCHED in iStatus.

The process of controlling this user interface is broken into sub-actions that allow a bunch of instructions to be executed and then instead of waiting for some action to complete—like writing a bunch of characters out onto the serial port—the display control hands the operation back to the “scheduler,” which then allows other processes to occur and after some period returns to the display control. The state variable that determines which action is being executed is called cMainDisplayPhase and is actioned via a switch statement which you can find in the UpdateMainDisplay function in the MainDisplay software module.

In describing the actions, I will refer to them by their case addresses, omitting the word “case” and line ending colon as I describe the actions of this process. Cases 0 to 7 simply set up the display and the keyboard matrix. The start of creating any screen is at WINDOW_SETUP_PHASE. It would be opportune to note that the variable cScreenPointer identifies which screen is being established.

WINDOW_SETUP_PHASE+1 and +2 write the text on the screen. The next two WINDOW_SETUP_PHASE+3 and +4 set up the windows on the screen and +5 and +6 format each of the windows in turn. The actual content of the windows could be set up here, as with the logo, or later as with the time that will need updating every minute. WINDOW_SETUP_PHASE+7 and +8 establish the buttons on the display.

— ADVERTISMENT—

Advertise Here

The WINDOWS_HOVER_PHASE is where this function idles away most of the time (Listing 5). The internal window process is a constant number located at Screen[cScreenPointer].cWindow[cWindowRotation] where cWindowRotation identifies each one of the four permissible windows. For each window in turn, the program checks if a window content update is required via the switch statement that calls a function that is exclusive to the desired window content as you can see in Listing 5. The update request is passed using a semaphore. In the case when the status changes, a semaphore is raised in the process.c module. The procedure UpdateStatus looks for this semaphore, and on seeing it changes the message in the status window on the display. There is an additional check if a key touch or flick has occurred.

case WINDOWS_HOVER_PHASE:
//once the system has been set up,
//the display process waits here for requests to update the display.
//based on the setup of each Window
//this approach means that a process like time can occur in any window 1-4
cWindowRotation++;
if (cWindowRotation>=MAX_NO_OF_WINDOWS)
{//increment ahead of the execution because
//there will be a move to a new display phase to send the serial data
cWindowRotation=0;
}
//prime the select window message
//although not sent if Default set below

ConstantStuffSquirtBuffer (1, 4, cSelectWindow);
cCompositeMessage[cCompositeMessagePointer]=cWindowRotation+1; //set screen
cCompositeMessagePointer++;
switch(Screen[cScreenPointer].cWindow[cWindowRotation])
{
case TIME_WINDOW:
UpdateTime();
break;
case STATUS_WINDOW:
UpdateStatus();
break;
case MENU_SELECTION_WINDOW:
UpdateMenu();
break;
.
.
.
default:
break;
}

//now looking if there has been a keyboard input
if (iStatus & SCREEN_TOUCHED)
{//need to remeber that a touch has been seen, but we need to
//hold off action until we know that it is not a flick
cMainDisplayPhase+=2;
}

break;

LISTING 5 – The WINDOWS_HOVER_PHASE is where this function idles away most of the time. The internal window process is a constant number located at Screen[cScreenPointer].cWindow[cWindowRotation] where cWindowRotation identifies each one of the four permissible windows.

The versatility of the system we have created allows the time/date to be in any window, and that the code associated with that window looks for the associated update flag. If an update is required, the executing subroutine will create a suitable message and post it on the serial port buffer and the next cMainDisplayPhase (WINDOWS_HOVER_PHASE+1) transmits the data out to the display and then returns to WINDOWS_HOVER_PHASE.

If a button has been activated, the operating sequence is changed to WINDOWS_HOVER_PHASE+2 (Listing 6). If the button or a flick has been seen, a switch statement based on the constant derived from the Screen structure associated with the current screen Screen[cScreenPointer].cKeyboardVector is used to determine how to treat it as you can see in Listing 6. Each function returns a value that can change the sequence of execution by modifying cMainDisplayPhase, the state variable.

case WINDOWS_HOVER_PHASE+2:
//scanning waiting for input
if (cShortTimer[FLICK_TIMER]==0)
{//wait for flick timer to end
iStatus &= ~SCREEN_TOUCHED;
//in case of multiple screen toucjes
if ((iStatus & FLICK_DETECTED)==0)
{
cI=cKeyboardDecode(cTouch[0],cTouch[1]);
switch(Screen[cScreenPointer].cKeyboardVector)
{
//every screen will vector here, the switch statemnet makes the case
//is unique to the screen and window
//use vector instead of for switch, because could use same
//interpetation for multiple screens.

case BASE_KEYBOARD:
cMainDisplayPhase=cInterpretBaseKeyboard(cI);
break;
case MAIN_MENU_KEYBOARD:
cMainDisplayPhase=cInterpretMainMenuKeyboard(cI);
break;
.
.
.
default:
cMainDisplayPhase=WINDOWS_HOVER_PHASE;
break;
}
}
else {
//flick detected
iStatus &= ~SCREEN_TOUCHED;
iStatus &= ~FLICK_DETECTED;
switch(Screen[cScreenPointer].cKeyboardVector)
{
//every screen will vector here, the switch statement makes the case
//is unique to the screen and window
//use vector instead of for switch, because could use same
//interpetation for multiple screens.
//
//This is adapted from above touch input- the method of determining which is the applicable window is the same

case BASE_KEYBOARD:
InterpretBaseFlick();
break;
case MAIN_MENU_KEYBOARD:
InterpretMainMenuFlick();
break;
.
.
.
default:
//cMainDisplayPhase=WINDOWS_HOVER_PHASE;
break;
}
ClearFlick();
//for the moment just go back
cMainDisplayPhase=WINDOWS_HOVER_PHASE;
}
}
break;

LISTING 6 – If a button has been activated, the operating sequence is changed to WINDOWS_HOVER_PHASE+2. If the button or a flick has been seen a switch statement based on the constant derived from the Screen structure associated with the current screen (Screen[cScreenPointer].cKeyboardVector is used to determine how to treat it as you can see in the following code extract.

The cInterpretBaseKeyboard function (in the case for BASE_KEYBOARD), for instance, looks for four possible types of inputs. I have set it up so that if you touch anywhere on the screen outside of the Status window or the two buttons, the system will transition to the MAIN_MENU screen. In this case the next screen is initiated by placing the current screen on the stack along with the next window’s PopStackLevel and then redirecting the function execution back to WINDOW_SETUP_PHASE in order to write the new screen.

If you touch inside the Status window it will toggle the display inside the status window from a summary to a verbose continuously updating status display. It does this by toggling a flag (cVerboseStatus) and simply returning to WINDOWS_HOVER_PHASE since the change in the window display will be handled in the UpdateStatus function.

Touching either the NEWER or OLDER button will send a semaphore that freezes the event generator to demonstrate how reviewing the history of events might look by scrolling the list up or down. In real life this is more likely to create a shadow buffer and present the data contained therein. A touch of a button moves the listing pointer one place.

The flick up or down works in a similar vein to the NEWER or OLDER buttons although depending on the travel distance of the finger flick, the pointer to the events is increased by 1, 3 or 5 places. I would recommend that you work through the complete listing to see how this works. It is just too long to go through here.

DISPLAY VARIABLES
Earlier I discussed the use of the “at” symbol (@) to indicate the location of a variable within a text line that will be calculated and then written to the display. These can be direct outputs like the version creation we discussed earlier or even the time and date that you can see in the code. But there are two other types of variable that can be shown on the display. In truth, they are very close but I have chosen to treat them differently rather than create a single procedure to handle both. Almost every largish project I’ve ever worked on has parameters that allow you to adjust the operation of the system.

For the first variable type the choice is a text-based option like: ACTIVE, INACTIVE, ONE DRAIN, TWO DRAIN and so on. These are not necessarily either/or options—there could be four or five different selections. These get boiled down to some numeric setting within the MCU, but it’s much easier for an operator to select based on the text that he can see.

The second variable type can involve significantly more information. My approach enables just one procedure to handle all the different types of parameters possible. At its simplest it may be just a numeric setting like the total number of floors in a building, but nevertheless it would be nice to have a text descriptor of the parameter. Obviously, there is a unique location in EEPROM associated with the parameter. The first issue is the range the parameter can operate over and if you are just incrementing or decrementing, what is the value of each step? What are the units?

To achieve this, I decided to embed the necessary information in the display line that will show the data. The 6 digits following the “@” are swallowed into the software and will not appear up the displayed line. The first two characters form an 8-bit hexadecimal number which is used to determine which process is performed. @01 will generate the version number, @02 and @03 will display the time and the date respectively.

The remaining four characters generate a 16-bit hexadecimal value. It was originally my intention to use this as a direct pointer to the EEPROM memory location. However, it quickly became obvious that that was too restrictive, so I changed it to a pointer to a set of data in yet another structure. The four characters are an overkill, but now historically enshrined. I have allocated the first two characters for the TextParameters to be “06”, and NumericParameters to be “07”.

So now we’re back to some more structures. The options to set a parameter form a list that is identical in approach to a Menu selection process that we’ve already seen, although not yet described. So instead of reinventing the wheel, the first member points to a menu and a new menu is created to handle the options. Menu lists are next up, so, have patience! iEEPROM_Pointer indicates where in the EEPROM the parameter is located.

struct TextParameters {
uint8 cMenuSelection;
//pointer to MenuItemList
uint16 iEEPROM_Pointer;
//EEPROM location
};

There are several more elements for the NumericParameters (Listing 7). As with TextParameters, iEEPROM_Pointer indicates where in the EEPROM the parameter is located. Some parameters are 8-bit numbers, others are 16-bit (if there are those with 32-bit, you are going to need to adjust the other elements of the structure as well). cParameterNumberOfBytes determines how many bytes are used for the parameter. iParameterUpperLimit and iParameterLowerLimit place bounds on the parameter and iParameterStepSize defines the amount added to or subtracted from the target each time the user increases or decreases the parameter.

struct NumberParameters {
uint16 iEEPROM_Pointer; //EEPROM location
uint8 cParameterNumberOfBytes; //Is the destination parameter 1 (=1) or 2 bytes (=2)
uint8 cUnits; //pointer to a unit in a list of possible options
uint8 cUnitConvertProcess; //if a conversion is required e.g. from degF to degC or cm to in
//this is the process vector as to how to convert and back again
uint16 iParameterUpperLimit;// the parameter has an upper limit
uint16 iParameterLowerLimit;//the parameter has a lower limit
uint16 iParameterStepSize;//the change is not necessarily 1
};

LISTING 7 – There are several more elements for the NumericParameters. As with TextParameters iEEPROM_Pointer indicates where in the EEPROM the parameter is located.

The constant cUnits is an index to a constant array cUnits (yes, the same name) and that the line in Messages that contains the units. That approach allows for a change in languages. The element cUnitConvertProcess is a number that will provide a vector for the conversion process. For instance, sometimes you may want to store a temperature in degrees Celsius, but display it in degrees Fahrenheit and the conversion process would cater to that. The actual units will be tagged onto the text line after the location where the parameter has been written.

MENU LISTS
Menu lists line up with the underlying touch keys, but when it comes to placing the text, the locating instructions are relative to the window and not the overall screen, so the constants used to create the keys cannot be used “as is.” Creating a menu list relies on yet another structure MenuItemList as shown in Listing 8.

struct sMenuItemList{
uint16 iNumberOfMenuItemsInList;
uint16 iMenuItemsList[MAXIMUM_MENU_ITEMS]; //different lines made up of text in several locations in Messages
uint8 cTextColour[3]; //this could be an array for different color lines in the menu, notimplemented here- stay with a single colour
uint8 cTextSize;
//this could be an array for different size characters in the menu, not implemented here- stay with a single size
uint16 iParameterScreen[MAXIMUM_MENU_ITEMS]; //punter screen PARAMETER_LIST_SCREEN or PARAMETER_NUMERIC_SCREEN
uint16 iParameterChangeLine[MAXIMUM_MENU_ITEMS]; //line to use for the parameter- remember the parameter details are embedded in the line
};

LISTING 8 – The full menu is created from lines of text and the numbers of those lines are included as the iMenuItemsList.

The number of lines in the menu iNumberOfMenuItemsInList is defined as the first item in the structure, keeping in mind that there can be a maximum of six lines on the full screen (six rows of touch keys) and the window may be limited to five or fewer rows. So, the window of the menu list provides a “window” of five of the options. It’s possible to scroll up or down the list using the “UP” and “DOWN” buttons (if they are provided on the screen along with the software) or by flicking up or down (Figure 2). The full menu is created from lines of text and the numbers of those lines are included as the iMenuItemsList. It would be possible to assign different colors and text size to each line, but I deemed that too complex and so cTextColour and cTextSize apply to the whole list.

FIGURE 2 – This is screen 1. Here, five items of a much longer list are displayed within a window. Touching the UP or DOWN buttons will scroll the list accordingly. BACK will move you back to the display of Figure 1. Flicking up or down will also move the list within the window. Touching any one of the selections will vector you to another screen (provided it has been coded, of course). Only two have been implemented for this development so far, the DWELL TIME and SET LANGUAGE. Tapping the former will take you to Figure 4 and the latter to Figure 3.

Obviously, some action needs to occur for each line if it is pressed and iParameterScreen points to the screen that will be used. At this point of the development, I am only considering two possible screens: one for parameters that consist of a list and one for numeric values. iParameterChangeLine points to the text line. Details on the actual parameter to be used are embedded in the text line.

If you look at the const instantiation of the sMenuItemList structure as MenuItemList you will see the 7th item set up to change the language of the display. At this point I ‘m only going to change a RAM variable, since I don’t have other languages implemented yet in the Messages array. As you can see it will use the PARAMETER_LIST_SCREEN and line 48 in Messages. Any touch on the screen is handled in the HOVER+2 phase of the state machine. Depending on whether a flick has been detected the case MAIN_MENU_KEYBOARD will be executed as a keyboard entry or a scroll up or down (flick).

As I have pointed out, it’s difficult for the MCU to read back and interpret what’s on the screen, so interpreting the selection made on the menu list requires knowledge of the actual lines on the screen. This is found by maintaining the pointer to the first line (iMenuLinePointer) and working with the offset relative to the top line in the window.

Changing a parameter with a set of different options is achieved using two windows as seen in Figure 3. The setting is displayed in the top window, with the current status shown. The list of options is shown in the second window and is nothing but a menu list, so the approach to display/selection is identical. There are four switches in addition to the list selection switches. Up and Down (as well as the flick) shifts the display list if there are more than five options. BACK simply backs out with no update. SET take the current value (as shown in the first window) and typically saves it to non-volatile memory.

FIGURE 3 – This is screen 2. The method for selecting a text parameter is to display the current setting in the top window and all the options in the lower window. The list can be far longer than the five lines displayed and can be shifted using the UP or DOWN buttons or by flicking up or down. As before, BACK will go to the previous screen without updating the parameter. Set will take the current setting and same it to the correct parameter in non-volatile memory and then back to the previous screen.

When updating a parameter, the item normally resides in flash/EEPROM and is only updated once the entry process is complete and the user is satisfied. To this end, the parameter is loaded into RAM and adjusted there until re-storage is required and then it’s written back to non-volatile memory. Because of the way I’ve implemented the states, I have found that I needed to introduce a flag (in wSemaphore) to read the parameter into RAM only once.

NUMERIC PARAMETERS
Some parameters are numerical values. The approach I have taken is to fetch and display the current setting. The value can be changed by nudging the value using the INC or DEC buttons or to enter the numeric value directly from a numeric keypad on the screen as you can see in Figure 4.

FIGURE 4 – This is screen 4. The first time a number is pressed, the current value is set to just that digit. As the number is entered so the digits move from right to left.

I’ve also placed additional information to the top window as a guide to the user. INC and DEC will change the value of the setting by the DELTA amount and will limit the adjustment to the maximum and minimum values. The software is designed for a 16-bit number so the maximum number that can be entered is 65,535 or the conversion goes wonky. If you enter a number greater than 65,536, the entry is set to zero, which you can also achieve by touching the CLR button.

The entered numbers will move from right to left as they are entered, similar to a calculator action. The BACK button will return to the previous menu without updating the parameter. SET will evaluate the number that appears relative to the maximum and minimum and with the range will save it to non-volatile memory and return to the previous menu. If the number is outside the range, the number will be zero and the display will remain.

The multitude of colors and text sizes you see are simply part of an evaluation to see how things look and feel. The hard part comes in trying to fix the appearance you—or more importantly, the customer—want and maintaining consistency. You will also find in the online resource a file call “Steps” which is a sort of step-by-step description of how to add a screen or a parameter.

There are still two possible constructions that are needed, but they seem to be a simple extension of what I have done. They are an alphanumeric keypad (as we saw in my first article) and it is required to enter a password or alphanumeric parameter information. Getting into that would add length to an already long article, and simply add more confusion.

So, now I have come full circle in describing the creation of hierarchical menus. I believe that the concepts are applicable beyond just the Noritake display and I hope that someone manages to implement them. 

Designing Touch Menus Using Excel
I developed techniques in this article to handle the changes that are part and parcel of the development of any project. All that said, life is always easier when your starting point is close to the end point. In order to help myself—and especially the customer—envision the display system and its use, I employed Microsoft Excel to help approximate the display’s operation.

Each screen is mapped to a tab of the Excel worksheet and I have chosen to indicate the 10 × 6 locations of the touch keys in yellow—even if they aren’t implemented. The worksheet ExcelDisplayHierarchy.xls can be found on Circuit Cellar’s article code & files download page. Where entities (buttons or data entries) span multiple touch keys, the Excel cells are merged—a simple technique, but in the interests of space not discussed here.

Where a touch is intended to link to a new display, I insert a hyperlink (using the standard Excel approach) in the desired cell as follows: In the pop-up dialog in Figure A select Place in This Document on the left and then click on which tab you want to be the destination. The tabs are listed in the area headed with: Or select a place in this document. I select the Type the cell reference as “A1” since by default I set the cursor on the top left cell of the sheet. The text that you enter in Text to Display will appear in the cell you have selected. The only point to note is it must be text—you can’t enter punctuation or a blank because Excel will enter it as a normal hyperlink and underline the text. However, all the formatting techniques can still be used so you can change the text’s color, size and so. Note that right clicking a cell will allow you to edit the hyperlink.

FIGURE A – The main screen of the display corresponding to Figure 1 in the main article. Overlaid is the Insert Hyperlink dialog.

In Figure A, touching anywhere on the screen outside of the Status area will bring up the SelectParameter tab (corresponding to Figure 2 in the article). If I wanted to “blank” the “aa” text that I used, I could change its color to the background color. You can see the hyperlink to the “aa” on most of the cells. Clicking on the text will bring up Figure B. Clicking on” DWELL TIME” will take you to the SetDwellTime tab and “SET LANGUAGE” will similarly go to the SetLanguage tab. Clicking on “BACK” will go back to the Main tab.

FIGURE B – Note the hyperlinks with the underlining. Buttons like “UP and “DOWN” are merely text. The other blank items in the menu list have not yet been implemented.

Using this technique, you can get a good approximation of how the system will work before embarking on coding the project.

RESOURCES

References:
[1] Hierarchical Menus in Embedded Systems, Circuit Cellar, Issue #160, November 2003
[2] gen4 Display Module Series 7.0” Diablo16 Integrated Display Module datasheet
[3] GT-C9xxP series “General Function” Software Specification (requires registration)  GT800X480A-C903PA Hardware Specification (requires registration)

Cypress Semiconductor | www.cypress.com
Noritake | www.noritake-elec.com

Links to more of Aubrey’s publications on/in Circuit Cellar, Planet Analog and Embedded.com at are available at: http://bit.ly/2m26MJB

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JANUARY 2019 #342 – Get a PDF of the issue


Don't miss out on upcoming issues of Circuit Cellar. Subscribe today!

 
 
Note: We’ve made the October 2017 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.


Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Become a Sponsor
Engineering Manager at | + posts

Aubrey Kagan has worked in electronics for more years than he cares to remember. He is currently Engineering Manager at Emphatec, an industrial electronics design house in Markham, Ontario. He has written many articles for Circuit Cellar over the past 25 years as well as a book Excel by Example based on three of those articles. Aubrey was one of the “notable contributors” interviewed in Circuit Cellar’s 25th Anniversary issue. He has also published several design ideas as well as numerous blogs covering many aspects of electronic design. You can find a list and links to more of his publications on the Circuit Cellar article materials webpage. He can be contacted at [email protected]