Front Panel
Front Panel
R&D Books
Lawrence, Kansas 66046
R&D Books
an imprint of Miller Freeman, Inc.
1601 West 23rd Street, Suite 200
Lawrence, KS 66046
USA
Designations used by companies to distinguish their products are often claimed as trademarks. In
all instances where R&D is aware of a trademark claim, the product name appears in initial capi-
tal letters, in all capital letters, or in accordance with the vendor’s capitalization preference. Read-
ers should contact the appropriate companies for more complete information on trademarks and
trademark registrations. All trademarks and registered trademarks in this book are the property of
their respective holders.
Copyright 1998 by Miller Freeman, Inc., except where noted otherwise. Published by R&D
Books, an imprint of Miller Freeman, Inc. All rights reserved. Printed in the United States of
America. No part of this publication may be reproduced or distributed in any form or by any
means, or stored in a database or retrieval system, without the prior written permission of the
publisher; with the exception that the program listings may be entered, stored, and executed in
a computer system, but they may not be reproduced for publication.
The programs in this book are presented for instructional value. The programs have been care-
fully tested, but are not guaranteed for any particular purpose. The publisher does not offer any
warranties and does not guarantee the accuracy, adequacy, or completeness of any information
herein and is not responsible for any errors or omissions. The publisher assumes no liability for
damages resulting from the use of the information in this book or for any infringement of the
intellectual property rights of third parties that would result from the use of this information.
ii
Chapter 3
27
28 — Front Panel: Designing Software for Embedded User Interfaces
the event queue and calls the appropriate function for each event as it is popped off
the queue. Much of the flow of control is hidden from the programmer, as it should
be. There is no reason for the programmer to know how the key or mouse events are
read, filtered, or queued by the system.
However, the embedded programmer is often faced with hardware designed
in-house that cannot be integrated with a commercial operating system. Many of the
principles implemented by the operating system or by GUI toolkit vendors are the same
as those you can implement in your own embedded system. The principles are the same
whether the interface is as complex as a full GUI or as simple as a digital watch.
void main(void)
{
initialize();
while (1)
{
e = readEvent();
if (e is a valid event)
{
processEvent(e);
}
}
}
This technique works in simple cases, but if processing an event takes longer than
the time for an event to occur, some events may be missed. Events cannot be queued
to be processed later. This gets more difficult if there are several kinds of events that
you need to prioritize. Because of these restrictions and because of its simplicity, this
approach is not discussed any further.
Scheduling and Managing User Events — 29
void keyPollTask(void)
{
while (1)
{
e = readEvent();
if (e is a valid event)
{
enqueue(e);
}
sleep(poll_period);
}
}
void eventProcessingTask(void)
{
while (1)
{
e = dequeue();
processEvent(e);
}
}
This arrangement allows the user to type ahead. The extra events sit on the queue
until the event-handling loop is ready for them. The flow of control can be seen in Fig-
ure 3.1. The event e2 was detected before e1 had been completely processed. If the
poll had not taken place before the processing of e1 was complete, the event could
have been missed completely. The polling period must be chosen so that no events are
missed. At the same time, the higher the frequency of polling, the more CPU cycles
are spent checking for events, even when no events have taken place. Missing events
is less likely if the hardware latches an event so that it can be detected an arbitrary
period of time after the occurrence. The latched data is then cleared by software so
that the hardware is ready for the next event. This is only a partial solution because
during the period that data is in the latch waiting to be read, no further events can be
detected. The latch has effectively provided a queue of length one.
30 — Front Panel: Designing Software for Embedded User Interfaces
If there are delays in the hardware during the readEvent() function, the control
can switch to the event being processed during those delays. A delay may be inserted
to allow an output signal to settle before reading the input. These context switches are
not shown in Figure 3.1.
If you are concerned that the polling interval you have chosen may miss events
occasionally, simply run the system with the polling set at half the design frequency. If
no events are missed, you can trust the design frequency. Such a check is worth the
effort, because operators will get quicker at using the device over the years, possibly
quicker than the testers. As well as decreasing the frequency, it is also useful to increase
the polling frequency during testing. Some motion detection devices may generate an
event on every poll. This stream of events may be in danger of flooding another part of
the system. Increasing the polling frequency stress-tests such conditions.
3.2.3 Interrupts
To use interrupts, the event-detecting circuit must be wired to one of the processor
interrupt lines, while the polling methods require only that the event-detecting circuit
be readable, usually using the address and data lines. An interrupt, unless disabled,
immediately switches the flow of control to the interrupt service routine (ISR). Simi-
lar to timed polling, the ISR queues the event until the main flow of control has time
to process it. Figure 3.2 shows the timing diagram for the same events with interrupts.
Polling Task
Polling period
Event-handling
Task
This timing diagram shows two events detected by the polling task.
The event-handling task processes each one in turn.
Scheduling and Managing User Events — 31
Note that there is no interrupt when there is no event. In this case, the time between
the detection of e1 and e2 is not predefined. Interrupts are useful when the event is
very brief and may be missed by a polling routine.
The interrupt mechanism can be used without an RTOS. Effectively, the inter-
rupt supplies one thread of control while the main flow of execution provides
another. Obviously, the interrupt mechanism can still be used if there is an RTOS
available, but note that it is not possible to place the interrupt’s thread of control into
the priority order dictated by the RTOS. This may be important if you do not want
the keyboard-scanning routines to interfere with other high-priority work that the
processor must perform. Disabling the interrupt during some high-priority sections
of the code may be an option, but this can increase the complexity of the code.
The overhead associated with polling, when there is no input present, might make
you think that the interrupt-driven system is more efficient. This is not always the case.
The average overhead in the interrupt-driven system is lower, but the average can be a
misleading metric. Consider a keypad that is polled every 10 milliseconds (ms). It takes
0.1ms to poll the keypad when there are no keys selected and 0.3ms to perform the poll
when there is a key present. So the overhead associated with the keypad is between 1
percent and 3 percent of the total CPU bandwidth. It is likely to be 3 percent when the
system is doing most of its work. This is because once the user presses a key, he is
likely to keep it depressed for several polls. Say, the user holds down a button for
300ms. During that time, the event is detected and enqueued to the event-handling task.
Interrupt Handler
Finish handling e1
This timing diagram shows two events detected by the polling task.
The event-handling task processes each one in turn.
32 — Front Panel: Designing Software for Embedded User Interfaces
The event-handling task processed that event. Say that event takes 250ms. The event is
complete, but during its processing, the overhead from the keypad is 3 percent because
the user kept his finger on the key. This is not unrealistic, because most people move
their fingers slowly relative to computer speeds. A similar scenario arises with motion
devices such as a mouse or a dial. While one movement is being processed, it is quite
likely the user has made another movement. The events from a motion device arrive in
bursts. For these bursts of activity, the timed polling approach has another advantage.
The change between two consecutive reads reflects the speed at which the device is
moved. This makes it easier to apply rate-sensitive algorithms to the response. This is
not true of an interrupt-driven approach because the time between successive reads var-
ies, and measuring that time may be nontrivial.
Now, look at the response in an interrupt-driven system. If the key input is not fil-
tered electronically, the interrupt may have to be debounced by disabling or ignoring
the interrupt for a certain period. Sometimes, it is not possible to disable the interrupt
without the risk of missing other events. If the interrupt is not disabled, many interrupts
may take up a large percentage of processor time at the exact moment an event needs to
be processed. If you can disable interrupts, you need to find an appropriate point in the
code at which the interrupts can be reenabled. If you wait until the event is processed,
you risk missing a second event.
If the period for which you disable the interrupts is the same as the poll period, the
overhead when the system is busy is the same for both designs. Picking the same figure
for the poll period and for the interrupt disable time is reasonable, because this is the
amount of time that can pass without missing an event. The performance during the rest
of the time is often irrelevant. If there is no system activity on the user interface, the
processor responsible for the user interface will have spare CPU cycles available.
For highly responsive systems, interrupts are the only way to detect events, but on a
user interface, you should always give timed polling serious consideration. The over-
head is bounded and predictable, the hardware is less complex, and most important, the
software is easier to write and, therefore, to debug.
3.3 Queuing
Whether the input event is detected in a polling task or in an interrupt, the request has
to be queued. If you have an RTOS, it provides a queue structure. The enqueue and
dequeue operations are atomic, avoiding any problems caused by simultaneous
access. If you do not have an RTOS, you can implement the queue by using an array
as a circular buffer. You also need some means of locking that data to ensure that it is
not read while it is being written. Disabling interrupts while the queue is being read is
the simplest way. Within the interrupt routine itself, other interrupts have to be dis-
abled while the queue is being written to protect against nested interrupts causing
overlapping writes to the queue.
Scheduling and Managing User Events — 33
The length of the queue depends on the nature of the input. In desktop systems, a
lot of type-ahead is appropriate, allowing the user to type commands or sentences.
Most embedded systems do not need more than one-key type-ahead. If key-up as well
as key-down events are detected, you may want to allow a longer queue. Motion
devices that generate a stream of events also require a longer queue. Once the queue is
full, most systems function if the following events are thrown away. Users soon real-
ize that they have limited type-ahead (or move-ahead, in the case of a motion device).
If you discard events, you must ensure that events do not depend on each other. If a
key-down occurs, the logic of the controller may be upset if the corresponding key-up
never occurs. If this is the case, the queue must be sized to cope with the highest pos-
sible input rates and never lose events.
A general-purpose queue contains elements of a fixed size. µC/OS allows only a
single void pointer as the element of the queue. In the example that follows, I main-
tain a separate array to hold the data and point to it from the µC/OS queue.
The structure placed on the queue defines a protocol between the physical layer
and the controller. Consider a control system with the display shown in Figure 3.3,
which illustrates the screen layout when running the keyq example program on the
companion disk. The user can view and set three parameters: temperature, pressure,
and flow. The LED lights when the key next to it is selected. The corresponding set-
ting then appears in the numeric display, and the user can adjust the values using the
arrow keys. If the display is not touched for 10 seconds, it times out, the numeric dis-
play goes blank, and all LEDs are turned off. The display is also capable of displaying
two different alarms: high temperature and high pressure. So the task managing the
87
Alarms
Pressure
Flow
display can handle three types of event: key events, alarm events, and timeouts. I can
place these three types of events on the queue using the following structures.
/*
Key Mapping
*/
typedef enum { TEMP_KEY, PRES_KEY, FLOW_KEY, UP_KEY, DOWN_KEY,
NO_KEY} KeyValue;
/* Packet Structure */
typedef enum {KEY_PACKET, ALARM_PACKET, TIMEOUT_PACKET} PacketType;
typedef struct {
PacketType type;
union {
KeyValue keyValue;
AlarmStruct alarm;
} data;
} QPacket;
By storing the data for the different types of event in a union, I can minimize the
size of the packet I place on the queue. Notice that there is no data associated with the
timeout. The fact that it occurred is sufficient information because there is only one
type of timeout.
and meaningless, handling it may consume processor time that should be spent on
some other activity. It is possible to manage such timing issues by carefully structur-
ing the code to provide for each of several functions that get processed in turn. How-
ever, this can lead to spaghetti code. If you have a function that takes a relatively large
amount of time, you may have to place a function call in the middle of it, which
passes control to an unrelated area of the software to allow it to meet some deadline.
Such out-of-place calls are a sure sign that it is time to invest in an RTOS.
Because an RTOS supports multiple stacks and multiple threads of control, it
allows one event to be halted while a more important event is processed. The task that
has been halted can resume from the point at which it was before the context switch.
Interrupts provide a similar ability, but with some limitations. An interrupt requires an
external event. Sometimes, a task is driven by an internal event, such as a
queue-not-empty event. Generating an interrupt to reflect the internal event may be
difficult and may require extra hardware. Interrupt handlers work well when the work
they must perform can be restricted to a small time-slice. If an event triggers a func-
tion that will take several seconds to process, interrupts will prove unsuitable.
One approach to the task decomposition is to dedicate one task to the user inter-
face, while a number of other tasks manage the other responsibilities of the processor.
This works if user events can always be processed in a very short time. A fast proces-
sor may mean that the computations resulting from the event can be performed
quickly, but interfacing with other hardware may slow things down considerably. This
leads to timing requirements at two levels, which in turn leads to at least two tasks.
Assume that you are required to detect all key events and that the mechanical charac-
teristics of the key dictate that the shortest period during which the key event is
detectable is 50ms. If key events are polled, then a task must run every 50ms, or
faster, depending on whether the events are latched by the hardware. The polling itself
is generally a quick job and does not cause any other deadlines to be missed. The sec-
ond requirement is that while the event is being processed, a far longer period is
needed. Events may vary dramatically in the amount of time they take to be pro-
cessed, but this variation must not interfere with the performance of other tasks with
real-time deadlines. There is a danger that a continuous series of user events may halt
any task with a lower priority than the event-processing task.
Therefore, you generally want the polling task to have a higher priority than other
tasks that might delay the polling long enough that a user event may be missed. The
event processing task should run at a lower priority than tasks that cannot afford to be
delayed by user events. You will see another reason for the relative priorities of these
two tasks after I examine the tasks in the keyq example in the next section. The keyq
example is so small that it would not normally require an RTOS, but it is used here to
illustrate some important issues.
36 — Front Panel: Designing Software for Embedded User Interfaces
Monitor
The three tasks of our example system communicate with one queue.
Scheduling and Managing User Events — 37
uses the DOS functions kbhit() and getch(). In a real embedded system, the hard-
ware would be accessed rather than using kbhit() and getch(). Note that the delay
at the bottom of the loop is not equal to the poll period. The poll period is actually the
delay at the bottom of the loop plus the time it takes for one execution of the loop.
Note that this loop has no delay. The blocking point is the call to OSQPend(), which
does not return until there is some data on the queue. Thus this task consumes no CPU
cycles while there is no activity on the queue. Once an event does appear on the
queue, this task is ready to receive it.
The monitor task spends most of its time monitoring the hypothetical process
driven by the device. Whenever the temperature or pressure exceeds the acceptable
limit, the alarm is posted to the queue. In the example on the companion disk, I sim-
ply raise the alarms on a timed basis.
These tasks are actually configured and launched from the main function, but the
details are highly µC/OS-dependent, so I will not dwell on them here. There is more
information on task initialization in Appendix A.
Scheduling and Managing User Events — 39
Polling Task
Polling period Read held off
by this amount
Handle e1 Handle e2
Event-handling
Task
This timing diagram shows two events detected by the polling task in which the
polling task's priority is lower than the event-handling task. e2 is detected later
than in Figure 3.1 and could possibly have been missed.
40 — Front Panel: Designing Software for Embedded User Interfaces
give real-time feedback of one of the monitored parameters. If some of the key-press
events take more than a second to process, the bar graph appears to freeze briefly. A
case could be made that one task should manage the key events while another updates
the bar graph.
Breaking up the responsibility for updating the user interface raises a couple of
issues. The hardware controlled by each task may be linked. The LEDs that make up
the bar graph may be controlled by the same controller chip that controls other LEDs
on the display. If two tasks simultaneously access that controller chip, the LED’s dis-
play may become corrupted. Another example would be a graphics screen driven by a
VGA controller. VGA controllers are not reentrant, so two tasks interleaving their
access might lead to unpredictable results.
The hardware can be protected by a lock at the time of the update. Such locks,
sometimes called resources, are supplied by most RTOSs. You can allocate one lock
for each piece of the user interface hardware, or a single lock can be used for the
whole front panel. Because all the updates to the user interface happen in a few
refresh functions, it is straightforward to pick the places at which the hardware needs
to be locked and unlocked.
A more complex issue is how to protect the model. Although the refresh functions
localize hardware access, the model tends to be updated from several places, which can
lead to a lot of locking and unlocking of resources. There is a small performance pen-
alty here, but a much bigger cost is the increased complexity of the code. The only way
to avoid the locks is to make each task own a portion of the model, and to make sure no
task needs to access the model belonging to the others. This is not always practical.
Once several locks are used, the biggest danger is that the locks dictate that the
tasks spend most of their time waiting for each other. When this happens, any
improved response time for the short events is lost because the short event must wait
for the longer event to complete and release its locks. In this case, the whole purpose
of sharing the user interface over a number of tasks is defeated.
Before allowing a number of tasks to access the model and the user interface hard-
ware, analyze the interactions to see if parallelism is really needed. In the bar graph
example, will the user notice a slight pause in the graph’s movement if he is concen-
trating on the response to the key he just pressed? If some of your events take more
than a second to execute, examine the work they are doing and decide if some portion
of it is unrelated to the user interface and could be spawned off to another task. The
next section studies an example of this.
monitor the motor speed until it is stable. The user interface is now paralyzed for
those few seconds while the motor accelerates because the mvc task cannot read the
queue while it is monitoring the motor speed.
A reasonable solution is to pass the responsibility for controlling the motor to
another task. The mvc task now makes the request to start the motor and then reverts to
processing the next user event. The response to the key event may also include a key
click or some other feedback so that the user knows that the key press was detected.
The mvc task still has the responsibility for lighting a LED once the motor has reached
its stable speed. This can be achieved via a second event that is queued from the
motor-controlling task to the mvc task. Figure 3.6 shows the path of the event. In
between item 2 and item 3, it is possible that the mvc task processed other events from
the queue. The choice to use one queue to carry the key events and the motor-controller
events was arbitrary; two queues could have been used, with the mvc task reading both.
Breaking up the processing of starting the motor this way means that the mvc task
has two short jobs to process instead of one long one. In general, the user interface
software is more responsive if it has many small events to handle rather than fewer
large events.
keyPoll 1
3 mvc
Motor
2
Controller
queue to this task, any other task in the system can make a request to it. The overhead
of placing the requests on a queue may prove comparable to the overhead of locking
resources, and the design that evolves is a good deal easier to maintain.
Figure 3.7 shows this architecture. The monitor could now hold the model data for
the alarms and instruct the view to display them directly. The request on the queue to
the view would indicate which alarm to illuminate.
The model for the settings does not move, however, and still lives in the
model/controller task. This could be seen as dividing the model into two portions, or it
could be seen as two separate models. Even though some of the model resides in the
monitor task, there is no need for any controller logic because none of the user events
affect the alarm display.
The protocol to the view allows any elements on the display to be activated. The
view task sees the LEDs simply as three LEDs and may not know about their relation-
ship to the temperature, pressure, and flow settings. When the controller decides to
display the temperature, the request to the view is to display the number 78. The view
is not aware of the meaning of the number, and it cannot distinguish one type of set-
ting from another. All the logic that allows settings to be manipulated is still in the
model/controller task.
The view task is a convenient place to manage any timed activity that is purely
visual, with no interaction with the model data, such as flashing LEDs. The view task
would wait on its queue of requests, as well as wait on a timer that indicates when the
state of the flashing elements should be toggled.
View and physical output layer have spawned off to another task,
which has its own queue.
Scheduling and Managing User Events — 43
Another possible use for the view task is multiplexing LEDs to reduce the number
of control lines necessary (Labrosse, 1995). LEDs are often multiplexed in hardware
by a dedicated LED controller chip, but multiplexing can also be performed in soft-
ware to reduce the system hardware cost. The view task is a good place to manage
this operation.
A queued protocol is trivial to convert to a protocol that can pass over a network or
serial link. Now that I have separated the view, I have opened up the possibility of
maintaining the model on a separate processor. This can be an attractive option for
graphics systems in which the rendering of lines and arcs can be CPU intensive. One
processor can be dedicated to managing the display, while all the application-specific
control is performed on a separate processor. In such an architecture, the requests on
the queue are at the level of requesting a line to be drawn between certain points, or an
arc to be drawn with a given radius.
If the order of events of differing types is related, keep them in the same
queue.
Even if you have several queues, each one is not restricted to one writer. Many
tasks can write to a queue, and one task can write to many queues. On the reading
side, the relationship is not so flexible. Even if your RTOS allows it, more than one
reader of a queue is bad practice — events that pass through the queue may go to one
of a number of tasks. The writer would not be sure which task received the packets.
44 — Front Panel: Designing Software for Embedded User Interfaces
Depending on the priorities of the tasks, one task may consume all the events and the
others may get none. By giving each its own queue, this sharing of events can be more
easily controlled. Figure 3.8 illustrates these configurations.
Two writers,
one reader – OK. One writer,
two queues – OK.
One queue,
two readers – not OK.
Scheduling and Managing User Events — 45
With this technique, many intermediate steps may be skipped. If the processor is
fast enough to keep up with the events, there will never be more than one event in the
queue and no events combined. As the processor falls further behind, the queue grows
longer and more events are combined on each pass. If the user moves the mouse
quickly, he will see the mouse redrawn only in a few positions. If the mouse is moved
more slowly, it is redrawn at each intermediate step. This behavior should be more
acceptable to the user than allowing the mouse displayed on the screen to lag the
movement of the user’s hand. Similarly, if a dial updates a numeric display, then the
faster the user turns the dial, the larger the numeric value change each time the display
is updated.
Take care if the intermediate values are recorded. Consider a drawing tool that lets
the user draw a freehand curve with a mouse or other pointer. A line is drawn that
joins all the points on the screen visited by the mouse. The optimization just described
might result in fewer longer lines between the points as a result of the merge, rather
than a greater number of short lines.
In order to implement this technique, it has to be possible to look at the top ele-
ment of the queue without removing it. This allows the main event-handling loop to
pop the element later if it is an event of a different type. Some RTOSs allow the pro-
gram to examine the top element of the queue without removing it. If yours does not,
you can add this functionality by adding a layer above the RTOS-supplied queues.
This layer pops an element and stores it when a read is requested. If the element is
subsequently popped, it returns the stored element as the top of the queue. This code
does not need to be made reentrant because it would be called only from the task that
is reading the queue, and as I have already discussed, there should be only one reader.
typedef struct {
int temperature;
int pressure;
int flow;
} Settings;
46 — Front Panel: Designing Software for Embedded User Interfaces
The alarms are updated simply by the handleAlarm() function, which is called from
the infinite loop of the mvc task, and which you have already seen.
This function makes simple changes to the model, which are then copied to the view
by the refreshAlarms() function. In the DOS example, this function prints the
strings on the screen. A real embedded system may turn lights on instead.
void refreshAlarms(void)
{
if (GS_alarms.highTemperature)
{
gotoxy(40,15);
puts(" HIGH TEMPERATURE");
}
else
{
/* Clear the alarm if it was displayed */
gotoxy(40,15);
puts(" ");
}
Scheduling and Managing User Events — 47
if (GS_alarms.highPressure)
{
gotoxy(40,17);
puts(" HIGH PRESSURE");
}
else
{
/* Clear the alarm if it was displayed */
gotoxy(40,17);
puts(" ");
}
}
/*
The GS_focusValuePtr points to the value in the model that is being
modified.
*/
static int *GS_focusValuePtr = NULL;
When a key event for one of the settings occurs, the focus is adjusted by setting
GS_focusValuePtr to point at one of the three values in GS_settings. If the key is
an arrow key, the value in the model is adjusted via the pointer.
case PRES_KEY:
GS_focusValuePtr = &GS_settings.pressure;
break;
case FLOW_KEY:
GS_focusValuePtr = &GS_settings.flow;
break;
case UP_KEY:
/* Increment the focus value if there is one */
if (GS_focusValuePtr != NULL)
{
(*GS_focusValuePtr) ++;
if ((*GS_focusValuePtr) > 100)
{
*GS_focusValuePtr = 100;
}
}
break;
case DOWN_KEY:
/* Decrement the focus value */
if (GS_focusValuePtr != NULL)
{
(*GS_focusValuePtr) --;
if ((*GS_focusValuePtr) < 0)
{
*GS_focusValuePtr = 0;
}
}
break;
default:
assert(FALSE);
}
}
3.8.2 Callbacks
A callback is a function called in response to an event. Its name stems from a module,
object, or subsystem wanting to say: “If event X happens, then call me back.” The
function to be called is then recorded in some way so that it can be invoked when X
occurs. If either the number of focuses or the number of enumerated values used by
the focus are large, it can become difficult to manage and callbacks may be a good
alternative. If the choice is among several actions rather than among several pieces of
data, you should consider applying callbacks.
This mechanism matches the requirements of buttons well. Each button is associ-
ated with a callback function; that is, the action to be performed when the button is
pressed. In the handleKey() function already discussed, a switch statement is used
to distinguish among the buttons. Unfortunately, the switch statement is static; it can-
not be changed at run time. To examine how callbacks can be used on a device in
which the buttons change meanings, I will use the example of a simple digital watch.
This digital watch in Figure 3.10 has three buttons. It has two modes: time-of-day
mode and stopwatch mode. Depending on the mode, the buttons change meaning. In
typical digital watch style, I have written each of the meanings on each of the buttons,
and the user has to figure out on the fly which one applies. Most digital watches use a
small icon in the LCD to distinguish modes. Because I wrote this example in text
mode in DOS, the mode is displayed as an explicit string below the watch face. Note
also that the time of day can be adjusted in the normal time-of-day mode. There is no
special mode for updating the current time such as you would find on most real digital
watches. Those simplifications aside, this is a suitable case for callbacks because the
buttons change their associated actions depending on the mode and on whether the
stopwatch is running. This watch displays hours and minutes in time-of-day mode and
minutes and seconds in stopwatch mode.
Temperature
Pressure
Flow
50 — Front Panel: Designing Software for Embedded User Interfaces
If each button has a function associated with it and those functions are stored in an
array of function pointers, then you can change them at run time to alter the behavior
of the buttons. The following definitions allow us to use such an array.
#define NUMBER_OF_KEYS 3
/*
This is the array that will hold the callback for each key
*/
static CallbackFn GS_keyCallbacks[NUMBER_OF_KEYS];
The keys are polled in the same fashion as the previous example, but the packets
on the queue are a little simpler because there are no alarms to pass. The timeout mes-
sage is replaced with a one-second timer used to drive the ticking of the watch. The
new packet is as follows.
Up-Reset
12:00
Mode Down-Start/Stop
Mode = Time-Of-Day
The left-hand button changes between time-of-day mode and stop-watch mode.
The two buttons on the right have different meanings, depending on the mode.
In stop-watch mode, the lower-right button can mean start or stop,
depending on whether the stop-watch is running.
Scheduling and Managing User Events — 51
/*
This packet does not require a union because there is only
one packet with an associated value.
*/
typedef struct {
PacketType type;
KeyValue keyValue;
} QPacket;
The data being manipulated is the time of day and the current time on the stopwatch.
Whether the time of day or the current stopwatch time is displayed is in the domain of
the view.
/* Model Data */
typedef struct {
int hrs;
int mins;
int secs;
} TimeOfDay;
typedef struct {
Boolean running;
int mins;
int secs;
} StopWatchTimer;
static TimeOfDay GS_timeOfDay;
static StopWatchTimer GS_stopWatch;
/* View Data */
typedef enum { TIME_OF_DAY_MODE, STOP_WATCH_MODE } DisplayMode;
static DisplayMode GS_displayMode = TIME_OF_DAY_MODE;
Even though the seconds of the time of day are stored, they are not displayed on the
watch face.
Now that the data structures are in place, I can initialize the callbacks as follows.
GS_keyCallbacks[BOTTOM_LEFT_KEY] = setModeToStopWatch;
GS_keyCallbacks[TOP_RIGHT_KEY] = incrementTimeOfDay;
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = decrementTimeOfDay;
The entries in the array are assigned to functions that I will now define. The two
mode-change functions, setModeToTimeOfDay() and setModeToStopWatch(), rede-
fine the buttons on the right-hand side of the digital watch to functions appropriate to
the new mode. The bottom-right button also changes roles, depending on whether the
stopwatch is running.
52 — Front Panel: Designing Software for Embedded User Interfaces
void setModeToTimeOfDay(void)
{
GS_keyCallbacks[BOTTOM_LEFT_KEY] = setModeToStopWatch;
GS_keyCallbacks[TOP_RIGHT_KEY] = incrementTimeOfDay;
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = decrementTimeOfDay;
GS_displayMode = TIME_OF_DAY_MODE;
}
void setModeToStopWatch(void)
{
GS_keyCallbacks[BOTTOM_LEFT_KEY] = setModeToTimeOfDay;
/*
Bottom left means start or stop depending on whether we are running.
*/
if (GS_stopWatch.running == TRUE)
{
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = stopStopWatch;
}
else
{
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = startStopWatch;
}
GS_keyCallbacks[TOP_RIGHT_KEY] = resetStopWatch;
GS_displayMode = STOP_WATCH_MODE;
}
The following two functions perform the tasks associated with the two right-hand but-
tons while in time-of-day mode.
void incrementTimeOfDay(void)
{
GS_timeOfDay.mins ++;
if ( GS_timeOfDay.mins == 60 )
{
GS_timeOfDay.mins = 0;
GS_timeOfDay.hrs ++;
if ( GS_timeOfDay.hrs == 24 )
{
GS_timeOfDay.hrs = 0;
}
}
}
Scheduling and Managing User Events — 53
void decrementTimeOfDay(void)
{
GS_timeOfDay.mins --;
if ( GS_timeOfDay.mins == -1 )
{
GS_timeOfDay.mins = 59;
GS_timeOfDay.hrs --;
if ( GS_timeOfDay.hrs == -1 )
{
GS_timeOfDay.hrs = 23;
}
}
}
The three stopwatch functions are associated with the two right-hand buttons while
in stopwatch mode. The bottom-right button is associated with startStopWatch() or
stopStopWatch(), depending on whether the stopwatch is running.
void startStopWatch(void)
{
GS_stopWatch.running = TRUE;
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = stopStopWatch;
}
void stopStopWatch(void)
{
GS_stopWatch.running = FALSE;
GS_keyCallbacks[BOTTOM_RIGHT_KEY] = startStopWatch;
}
void resetStopWatch(void)
{
GS_stopWatch.mins = 0;
GS_stopWatch.secs = 0;
}
The infinite loop of the mvc task is of the same form as the temperature/pres-
sure/flow example, but the handleKey() function has changed dramatically. Instead
of having a large switch statement to handle all possibilities, it now has only to acti-
vate the appropriate callback.
This function is simple because all buttons use callbacks. In some cases, you may
use a mixture involving a switch statement to cover the keys or events whose mean-
ings never change and the callback mechanism for the remainder.
The remainder of the code for this example manages the timing for the seconds
ticking by and for refreshing the display, neither of which relates to the callback
mechanism directly, so I will not investigate it further. You will see more of the call-
back mechanism when you look at the code for buttons on graphical displays.
One word of caution about using callbacks: They make the flow of control harder
to predict by reading the code. In the example just given, any meaning could be attrib-
uted to a button at run time, and there is no way in C to limit that flexibility. Program-
mers who use Smalltalk see this as a natural form of message passing. However, it
makes tracking defects difficult. If you have a set of callback functions for mechanical
buttons and a completely different set of callback functions for graphical buttons, you
may want to guarantee a compiler warning if you assign a callback of one type when
a different one is expected. This is possible if you give the two types of callback dif-
ferent signatures, as in the following example.
The second line of the function generates a warning because the assignment is
expecting a function that takes an integer as an argument. This does not save you from
assigning one of the mechanical button callbacks to the wrong button, but at least it
avoids some more obvious errors. In Chapter 7 when I discuss C++, you will see call-
back mechanisms that are much safer.
Chapter 4
55
56 — Front Panel: Designing Software for Embedded User Interfaces
the function associated with this new state. This loop continues with each state, giving
each function a chance to run. Such an implementation is a poor man’s RTOS. Using
an FSM in such a way works on small systems but does not scale well, and it makes it
difficult to implement any sort of priority scheme. FSMs are completely unsuitable if
some of the threads of control have hard real-time deadlines and others do not. In such
a scenario, an RTOS is required. I describe this type of FSM to distinguish it from the
FSMs I am concerned with in this chapter. The FSMs implemented here are used
purely to manage the sequence of inputs from the user and have no effect on schedul-
ing CPU time.
input = Lock
input = Close
Open Closed Locked
input = Unlock
Finite-State Machines and Table-Driven Software — 57
You should always be aware of how visible the FSMs are to the user. Some FSMs
are used to manage processes of which the user is not aware. In other cases, I want the
current state of a machine, or machines, to be visible to the user. For example, the ele-
ment currently highlighted in a menu represents a certain state. Sometimes, the state
itself is not visible, but the transitions are visible to the user. Each transition may be
associated with an action function, and that function may interact with the user, mak-
ing the user aware that a transition took place.
I will repeat the door example here with a slightly different syntax to show a func-
tion call associated with each input. Let us assume that the door FSM must generate
the appropriate noises when the user makes a change. Those noises are generated by
the functions Slam() and Click(). Figure 4.2 shows this slightly more capable FSM.
For the purists, an FSM that has an action associated with each transition is a
Mealy FSM. Alternatively, you may associate the actions with each state. Such an
FSM is called a Moore FSM (Hendricksen, 1989). I will not discuss the Moore FSM
variety further.
Lock/Click()
Close/Slam()
Open Closed Locked
Unlock/Click()
58 — Front Panel: Designing Software for Embedded User Interfaces
Much of the time, the user is aware of this state information. He realizes that
pressing the Quit button while in a submenu has a different effect than pressing the
Quit button while at the top-level menu.
user, this is an important trade-off, because the state diagram can often be used to doc-
ument the behavior of the interface. Unfortunately, many real-world programs have
characteristics that are too complex to model with a simple FSM. More complex
mechanisms must be used. The menu example later in this chapter shows such a system.
Button
Up
Pr
es
) s/d
e( oN
at ot
iv
act hi
ng
Button e
e/d Sl ()
as id
le eO
Re ff
/do
No
th
in
g(
Pending_Up ) Pending_Down
Pr
es
s/d
oN
ot
hi ()
ng te
() va
Sl
id cti Button
eO e /a
ff as
/do le
No Re
th
in
g(
) Down
Button
62 — Front Panel: Designing Software for Embedded User Interfaces
The button images are shown next to each state. Drawing the image could be per-
formed by copying prestored bitmaps. In the example code, the image is drawn by
constructing a number of lines, rectangles, and polygons. The drawing method is not
relevant to the code to manage the FSM. The button’s appearance reflects the state, so
it is straightforward for the user to see each state. The 3-D effect is useful to distin-
guish touch-sensitive buttons from other labels on the screen that are not touch-sensi-
tive. At this point, I suggest that you run the fsm program supplied on the companion
disk. It allows you to view the button in each of the described states by selecting it
with the mouse.
The possible inputs are Press, SlideOff, and Release. SlideOff occurs when
the user moves his finger out of the area of this button. Whether he enters the area of
another button is immaterial to this FSM, because this FSM is concerned with only
one button, not with the entire set of buttons on the screen.
These inputs cannot be read directly. As each finger position is read from the dis-
play, it is parsed, or filtered, to detect any higher level events it represents. The button
object may have functionality apart from the FSM that indicates whether the button
area contains the point selected by the finger. A function can then process each posi-
tion, establish exactly which buttons are affected by the latest event, and generate the
appropriate inputs. Such processing, although interesting, is not the subject of this
chapter; see Chapter 5 for further details.
By happy coincidence, this button works nicely if you treat a finger sliding into
the area of the button exactly the same as a Press. This avoids having to create
another input value. If the user slides his finger over the display, entering and leaving
several Up buttons, the only one that transitions into the Down state is the one in the
Pending_Down state when the finger is lifted from the screen.
a more general FSM-processing function. Most real-world cases involve some excep-
tions. For example, you may wish to change the rules that decide when transitions are
legal. One enhancement is allowing the action function to return a Boolean value and
allowing the transition only if the action function returns TRUE. In other cases, you may
wish to transition to a certain state if an illegal input is received.
If there are many transitions, you may want to sort them and allow a binary search
for the matching input, or you may wish to sort them according to frequency of use, to
allow the most frequent transitions to be found soonest in a linear search.
Amid all the special-case processing, the few lines of code that actually call the
action function and change the state represent a small percentage, so the code reuse
advantage is minor.
Each state has a list of such transitions. Each transition is checked for an input match-
ing the user input. When a match is found, transitionFn is called and the state is
assigned to nextState.
The following assignments represent the full set of transitions. Note that the final
entry in each array starts with a NullInput value. This is a terminator and the rest of
the last line is not used.
64 — Front Panel: Designing Software for Embedded User Interfaces
The button itself is represented as a structure. Although one FSM can be used by all
buttons, I must create one Button structure for each button.
struct buttonStruct;
typedef struct buttonStruct Button;
struct buttonStruct
{
int x;
int y;
char *text;
ButtonState state;
void (*actionFnDOWN)(Button *buttonPtr);
void (*actionFnUP)(Button *buttonPtr);
};
The state field in this structure represents the current state of this button within the
FSM. The two function pointer fields are used to call particular functions once the
button has reached the Up or the Down states. There is no function for the
Finite-State Machines and Table-Driven Software — 65
The three transition functions follow. Some transitions perform no action. When the
button is placed in the Down state or the Up state, one of the button actions takes place.
The actionFnDOWN and actionFnUP pointers may point to any function that takes no
arguments and returns void. These are the callbacks for this button. These functions
perform the application-level work in response to the user’s action on the button.
66 — Front Panel: Designing Software for Embedded User Interfaces
Note that two levels of function pointers are traversed here. The first level uses
pointers stored in the FSM table. These are the same for all buttons and represent the
controller communicating with the model. The second level are functions associated
with each button. These may be different for each button, allowing each button to
have different application-level behavior. In the example, the FSM’s action functions
simply call the buttons actionFnUP() or actionFnDOWN(). In a real case, the button’s
activate() and deactivate() functions may sound a key click, change the cursor’s
appearance, or add other properties to the button.
Menus were once a common way to navigate text interfaces on desktop comput-
ers. More modern GUIs allow pull-down menus. These menus often give the user
access to a dialog to complete the request. Before the age of the GUI, menus often led
to submenus, which led to sub-submenus. That’s because tick boxes, radio buttons,
and other visual gizmos used to construct a request on a GUI were not available on
text displays. Many options on a GUI can be shown at the same time, while a hierar-
chical menu must go through questions one at a time.
Despite the disadvantages, a hierarchical menu is still a necessary tool in embed-
ded systems in which the display device may be a one-line text display or a serial link
to a terminal. Even if a full screen is available, the processing ability of the embedded
CPU or the amount of development time may limit the interaction to text rather than
graphics. The inputs available may be a couple of keys rather than the full keyboard
and mouse available on a typical desktop computer.
Long lists cause navigation difficulty in menus, but deep menus cause more
navigation difficulty. Never add depth to reduce width.
A common navigation aid in menus is to number the items in the menu. This lets
the user know how many options he has viewed and whether the menu has wrapped
around. However, the number may be misconstrued as a value. If the user sees “3
Pressure” as the third menu item, there is a possibility that he may assume that the
value of the pressure is 3. The user may not realize that he must go down one level in
the menu to see the actual pressure value.
If you want to be adventurous, you could try inserting earcons (an icon that you
can hear) into your menus (Brewster et al., 1996). A sound made in response to a key
click in the menu may help prompt the user. This could replace the key click. You
would not want a different sound for each menu item, but the sound at a leaf might be
different from the sound on reaching a node. The top-level menu could have a unique
sound to let you know that you cannot go further up the hierarchy.
The table consists of several nodes that form a tree. The tree analogy allows us to
use the term branches for options at any level and the term leaf for a node with no fur-
ther branches. Each node contains pointers to its parent node, to one child node, and
to one sibling, which is the next node on the list at its own level. The parent node
points to only one child, and from that child it can reach the other children. This is
more convenient than maintaining one pointer per branch in the parent node, because
the number of branches will vary.
A pointer to the action function is executed when the node is selected. Only leaves
are allowed to have action functions. There is also a string, which is the name of the
node and is used for the display.
I use the convention that down means further into the menu structure, up means
back toward the root of the menu, and next means the adjacent branch on the current
level. Although these definitions of up and down mean that the tree is upside down, it
is the usual way to draw menu trees.
struct menuNode
{
/* parent node */
struct menuNode *upPtr;
/* default selection of the list of child nodes */
struct menuNode *downPtr;
/* The next option in the menu at the current level. */
struct menuNode *nextPtr;
/* The function to call if this leaf is selected.
The actionFn must be NULL if this is not a leaf */
void (*actionFn)(struct menuNode *nodePtr);
/* The name of this node for display purposes */
char * string;
/* If this node is a leaf, and has a value associated
with it then it is pointed to by valuePtr.
It is a pointer to void since the type of the
value may vary */
void *valuePtr;
};
typedef struct menuNode MenuNode;
MenuNode mColor={NULL,&mColorSole,&mSettings,NULL,"Color",NULL};
The parent is NULL, indicating that this node is at the top level. mColorSole is the
first node in the list of children of this node. mSettings is the next item in the list at
the current level. The actionFn is set to NULL, because this is not a leaf. The name of
this node "Color" is displayed to the user when this node is navigated. The final
NULL is the pointer to the value associated with the node, which, again, is unused in
this node.
70 — Front Panel: Designing Software for Embedded User Interfaces
Figure 4.4 shows the menu tree. This is the conceptual shape of the tree and repre-
sents the menu as the user sees it. The pointers stored in each node do not connect in
that way, however. The tree in Figure 4.4 requires variable numbers of down pointers,
which involves list management. It is simpler to link the siblings, as shown in Figure
4.5, which shows lines representing the actual pointers. The up pointers are not
shown. Any node with a NULL down pointer is a leaf and has an associated action
function and possibly an associated value.
The table is represented in code by the set of declarations in Listing 4.1. To
resolve forward references, all the structures must be declared before they are defined.
They are declared extern before the set of definitions.
GS_currentNodePtr points at the current node. This gives us a point from which
to navigate. It is declared as follows.
Speed Pressure
This diagram shows the menu tree, which is only two levels deep.
Finite-State Machines and Table-Driven Software — 71
void menuNext(void)
{
GS_currentNodePtr = GS_currentNodePtr->nextPtr;
}
void menuDown(void)
{
if (GS_currentNodePtr->downPtr)
{
GS_currentNodePtr = GS_currentNodePtr->downPtr;
}
else
{
GS_currentNodePtr->actionFn(GS_currentNodePtr);
}
}
void menuUp(void)
{
if (GS_currentNodePtr->upPtr)
{
GS_currentNodePtr = GS_currentNodePtr->upPtr;
}
}
Pressure Brightness
Size Model
This diagram shows the menu tree, which is only two levels deep.
Listing 4.1 The table for a text menu as represented in code by a set of declarations.
/*
UP DOWN NEXT ACTION_FN STRING VALUE_PTR
*/
MenuNode mColor={
NULL, &mColorSole, &mSettings, NULL, "Color", NULL };
MenuNode mSettings={
NULL, &mSetPressure, &mDetails, NULL, "Settings", NULL };
MenuNode mDetails={
NULL, &mDetailsSize, &mExit, NULL, "Details", NULL };
MenuNode mExit={
NULL, NULL, &mColor, exitProgram, "Exit", NULL };
/* Expand out the Color sub-menu */
MenuNode mColorSole={
&mColor, NULL, &mColorUpper, selectColor, "Sole", &GS_trainer.soleColor };
MenuNode mColorUpper={
&mColor, NULL, &mColorLace, selectColor, "Upper", &GS_trainer.upperColor };
MenuNode mColorLace={
&mColor, NULL, &mColorSole, selectColor, "Lace", &GS_trainer.laceColor };
/* Expand the Settings sub-menu */
MenuNode mSetPressure={
&mSettings, NULL, &mSetBrightness, selectInt, "Pressure", &GS_trainer.pressure };
MenuNode mSetBrightness={
&mSettings, NULL, &mSetPressure, selectInt, "Brightness", &GS_trainer.brightness };
/* Expand the Details sub-menu */
MenuNode mDetailsSize={
72 — Front Panel: Designing Software for Embedded User Interfaces
Each leaf performs a task, while the internal nodes exist solely to allow naviga-
tion. Some leaves manipulate a field of the GS_trainer structure. Others just display
more information.
Several action functions implement option lists. When the user presses the Down
key on an option list, the current value is displayed. The Scroll key can be used to
view all possible values. Two asterisks mark the currently selected value, so the user
can distinguish it from the other options that he can scroll through.
By pressing the Down key on a menu item, the user may choose the value to apply
to the GS_trainer structure. For example, pressing the Down key while the display
contains Settings>Pressure = 7 sets the pressure field to the value 7. The display
changes to Settings>Pressure = 7 ** to indicate that the new value has been applied.
I need a function to handle changing values. There are two types of values handled
in the example program: integers and Colors. I will show the code for manipulating
integers; the code for Colors follows the same form. The first function, selectInt(),
is an action function pointed at by the menu structures. The second function,
adjustIntProcessKey(), handles user events once it establishes that he is trying to
change an integer. If the events must be directed to the adjustIntProcessKey()
function rather than one of the navigation functions, GS_eventHandlingFnPtr is set
to point to the adjustIntProcessKey() function. The controller checks this value
before performing any navigation. You can think of selectInt() as the setup func-
tion, while adjustIntProcessKey() does most of the real work involved in changing
a value.
Because the two functions must communicate with one another, GS_proposedInt
cannot belong to either one. It, therefore, has file scope.
The problem with this function is that if control does not revert to a central event
loop while the user adjusts the current value, that task will not get an opportunity to
process events other than the ones allowed for in the selectInt() function. If I
added a single Exit key that would exit the menu from any point, I would have to
change code in the main event loop. I would also have to update selectInt(). This
would not be a satisfactory arrangement, especially because selectColor() would
probably be coded the same way, leading to three updates. I am going to such pains to
illustrate this mistake because it is so common.
A couple of other functions accessible from the Details menu display the size and
model of the trainer. They have a simple job of displaying the menu with the extra
information tagged on to the end of the menu line.
76 — Front Panel: Designing Software for Embedded User Interfaces
/*
Currently there is only one model of this trainer. It is called Speedy, so we
can hardcode this piece of information in the leaf for details/model.
*/
void displayModel(MenuNode *nodePtr)
{
strcpy(GS_viewString, " = SPEEDY");
}
The exitProgram() function, accessible from the top level of the menu, provides a
trivial way of escaping from the menu program.
Color>Sole
void menuUpdateDisplay(void)
{
MenuNode *menuNodePtr = GS_currentNodePtr;
char displayString[MENU_TEXT_LENGTH+1];
char stringSoFar[MENU_TEXT_LENGTH+1];
/*
displayString would not need to be initialised here except for
the case in which we are in the main menu and the while loop
below will has zero iterations.
*/
menuNodePtr = menuNodePtr->upPtr;
}
/*
If a string was set in GS_viewString, append it to
the end of the display string.
*/
strcat(displayString, GS_viewString);
/* Ensure that the string never overruns */
assert(displayString[MENU_TEXT_LENGTH] == '\0');
/* Print the string on the display. */
textDisplay(displayString);
}
78 — Front Panel: Designing Software for Embedded User Interfaces
In order to make the menu a stand-alone executable program, the module menu-
main.c implements a loop capturing all keyboard input and passes it to the controller
(i.e., the menuProcessKey() function). The menumain.c module also defines text-
Display(), which displays the output on the PC screen.
Finite-State Machines and Table-Driven Software — 79
Experiment with table contents until you find a structure that matches
your needs.