Programming the high-level User Interface
The objectives of this lab exercise are:
- To write the highest-level user interface (UI) functions (device drivers) for the T1 target system
- To learn how the keypad and LCD function in the solution to the design problem of section 1.9
- To apply what has been learned about computing thus far
- To gain experience programming in C
The T1 target system will be required, but only the components listed in appendix B are needed for this lab exercise. Set up each component in accordance with the instructions for your specific T1 version in section A.2. Connect your components in the manner described in the “Online Resources” section of the web page for this lab exercise: https://rtcbook.org/ph.
Introduction
Very often in an interaction between a computer and a user, a message
or “prompt” is written on a display and the user is expected to respond
by entering an appropriate decimal number through a keypad. In this lab
exercise, you will write a C function, called double_in()
, to
perform the complete keypad/LCD procedure.
In this and the next two lab exercises, we will also write the
functions needed to implement double_in()
. For an
overview of how these work in context of the design problem, see
section 1.9, section
1.11. These
functions are provided by the T1 C library (section
0.6, section 1.11) in executable form
(i.e., without source code), but we will be replacing the library
versions as we work through the lab exercises. This allows us to use the
lower-level functions without writing them first.
With reference to the functional UI design (see figure
2.2), we are writing the user “input numbers” function, double_in()
, and the
“display prompts and messages” function, printf_lcd()
. These
functions have already been described briefly in section
1.11. We will also write a main()
program to
test double_in()
and
printf_lcd()
.
These functions should be written in the main.c file of the “stub” project workspace/lab1.
Writing the double_in
function for user number entry
The double_in()
function
will interactively get a string of characters from the user’s key
presses on the keypad ( in the introduction of this book),
displaying them immediately to the LCD (). Here is a
description of how the function should work:
A user prompt (a string of ASCII characters) should be written on line 1 of the LCD. A pointer to the string corresponding to this prompt should be the only argument of the
double_in()
function.A floating-point number should be accepted from the keypad in response to the prompt. The number is entered as a string of ASCII characters that may include the decimal digits \(0\)–\(9\), a decimal point, and a minus sign, and is terminated by .
If the input string contains an error, the display should be cleared, an error message written on line 2, and the prompt issued again on line 1.
This string should be converted to the equivalent floating-point number: C data type
double
.The return value of
double_in()
is the floating-point number.
For instance, two calls to double_in()
, using a
string literal, might be
= double_in("Enter Vel:");
vel = double_in("Enter Pres:"); press
The values entered by the user would be assigned to the variables
vel
and press
.
Therefore, the prototype of the double_in()
function
should be
double double_in(char *prompt); // prompt is a string pointer
A general strategy for double_in()
is given
in . The details that follow help clarify
how to write the double_in()
program.
Handling keypad input
Under normal circumstances (no user entry errors), double_in()
should
work as shown in figure 1.14. The string prompt
input is printed to the LCD with
printf_lcd()
,
a user keys in a floating-point number and presses
, fgets_keypad()
gets
the string from the keypad, and then the C library function sscanf()
converts
this to a double
floating-point number, which is return
ed to the
calling program.
The prototype of
fgets_keypad()
ischar * fgets_keypad(char *buf, int buflen);
The first argument is a pointer to a string (buffer) that holds the user-entered characters. The second argument is the maximum number of characters to be read (including the null character) into the buffer.
The prototype of
sscanf()
1 isint sscanf(const char *s, const char *format, ...);
The first argument
s
is a pointer to a string (e.g.,"50.75"
); the second argument is a conversion format specifier (e.g.,"%lf"
for a long float, adouble
); and the third argument should be a pointer to adouble
variable that will store the converted floating-point number (e.g.,&val
for adouble val
).
Handling input errors
When designing a user interface it is important to anticipate
possible entry errors. When an invalid number is entered, double_in
should operate as illustrated in
figure 1.15.
In the case of entering a floating-point number on the keypad, there
are four possible entry errors, as shown in table 1.11. The
table also specifies an error message for each case. The double_in()
function
must detect each of these errors, print the corresponding error message
on the LCD, and prompt the user again.
Our goal here is that the user must enter a valid number string
before it can be converted to a double
and returned
to the calling program. Detecting error conditions is a matter of
testing the string written by fgets_keypad()
.
Three functions from the C standard library header string.h2 will be helpful for
these tests. The strpbrk()
function3 prototype is
char* strpbrk(const char *s1, const char *s2);
The function looks for the first occurrence of any character of
string s1
in string s2
and, if found, returns a pointer to the
string. If none is found, strpbrk()
returns
the null pointer, which is typically given
the constant NULL
. The and
keys will be assigned ASCII characters [
and ]
; therefore, if we
look for "[]"
in a
string, strpbrk()
returns
NULL
only if neither character appears
in the string.
Using strpbrk()
to detect
the presence of bad characters (keys) works well. However, we must also
detect multiple decimal points. One decimal point is valid, but two is
too many. The strchr()
and strrchr()
functions4 are useful for detecting multiple
decimal points. Their prototypes are
char* strchr(const char *s, int c); // first occurrence of c in s
char* strrchr(const char *s, int c); // last occurrence of c in s
These look for the first (strchr()
) or last
(strrchr()
)
occurrence of character5 c
in string s
and return a pointer to the
character, if found. Otherwise, they return NULL
. For example, the following program
prints the substrings corresponding to the first and last occurrences of
the character 't'
in the
string "attracting"
.
int main(void) {
const char *s = "attracting";
char *tf = strchr(s, 't');
char *tl = strrchr(s, 't');
("Truncated '%s':\nat first 't': %s\nat last 't': %s",
printf, tf, tl);
sreturn 0;
}
Its output is
Truncated 'attracting':
at first 't': ttracting
at last 't': ting
How might we use strchr()
to
determine if the user’s string contains a minus sign ('-'
) past
the first character? How might we use strchr()
and strrchr()
to
determine if the user’s string contains multiple decimal points?
Hint: Consider what strchr()
and strrchr()
return
when they both find the same decimal point. What if there is no decimal
point? What if there are multiple decimal points?
Putting all these ideas together, let’s expand into a more detailed strategy for double_in()
:
Begin by using the
printf_lcd()
function to clear the LCD screen (see subsection L1.4 for the list of escape sequences that can be used).Set a flag indicating that the keypad entry process has not been completed. For example, using an integer variable:
err = 1;
Enter and stay in a
while
loop whileerr == 1;
Within the loop (as described next), prompt the user and issue error messages until a string with no errors has been entered, and then set the flag
err
to 0.After the loop, use
sscanf()
to perform the ASCII-string-to-double conversion, and return the result.
Hint: Recall that becausesscanf()
is converting to a variable of typedouble
, you must use the format%lf
(long float).
Within the while
loop:
Use
printf_lcd()
to move the cursor to the start of line 1 and display the prompt. Then, check for the four specific errors as follows:Use
fgets_keypad()
“Get String” to obtain the string from the keypad.Note: If no digits are entered,
fgets_keypads()
returns a NULL, a string of zero length.Use the
strpbrk()
“String Pointer Break” to detect or in the string. Note: is returned byfgets_keypad()
as the ASCII character'['
, and as']'
.Use the
strchr()
to detect minus signs'-'
the first character.Use the combination of
strchr()
andstrrchr()
to detect multiple decimal points.
If any of the four errors occur, clear the display, move the cursor to line 2, and print the appropriate error message shown in Table 1.11. Alternatively, if the user-entered string passes all of the tests, set
err=0
.
Note: printf_lcd()
and
fgets_keypad()
work like the standard C functions printf()
and fgets()
, and they
are linked to your program from the T1
C library.
Writing the printf_lcd
display function
Our second task is to write the printf_lcd()
function used by double_in()
. The C
standard library function printf()
prints to
the standard output device (in our case the Console pane of the Eclipse
integrated development environment (IDE)).
We want printf_lcd()
to
operate exactly as printf()
, except
that it will print to the LCD. To do this, we want printf_lcd()
to
accept a format string with a variable number of arguments. Therefore,
the prototype for printf_lcd()
will be
int printf_lcd(const char *format, ...);
The format
argument is a format
string specifying how to interpret the data, and the ellipsis ...
represents the
variable list of arguments specifying data to print. The return value is
an int
equal
to the number of characters written if successful, or a negative error
code if an error occurred.
For example, to clear the display, move the cursor to the beginning
of line 1, and print float
s a
and b
,
we could write
= printf_lcd("\fa = %f, b = %f", a, b); n
Broadly, our strategy for implementing printf_lcd()
is as
follows:
Use the C function
vsnprintf()
to write the data to a C string.Use the LCD driver function
putchar_lcd()
to successively write each character in the string to the LCD.
Parsing
the variable argument list with vsnprintf()
The C function vsnprintf()
writes
formatted data from the variable argument list to a buffer (string) of a
specified size.6 For instance, after invoking va_start()
with
argument pointer ap
, char string[80]
,
and int n
,
va_list args;
(args, format);
va_start= vsnprintf(string, 80, format, args);
n (args); va_end
This writes the arguments to a formatted string
of length 80
char
s according to
format
(i.e., the first argument of
printf_lcd()
).
If successful, the returned value n
is
the number of characters written in the string. However, if an encoding
error occurs, a negative error code n
will be returned.
Printing
each character with putchar_lcd()
Now that the arguments are parsed, we must write the resulting string
to write to the LCD. The lower-level display driver function putchar_lcd()
writes
a single character to the LCD. Placed in a loop, putchar_lcd()
can
write successive characters of the string until the \0
null character
that ends the string is encountered.
Although array subscripts and the standard C function strlen
might be used to implement this loop,
the use of pointers makes the task simple and efficient. Here is a
useful idiom for looping through an entire string
using a pointer p
:
char *p = string; // point to string start
while (*p) putchar_lcd(*p++); // loop/put until char is \0
Notice that the \0
char
evaluates to
false (whereas all others are true), so this loop stops when the string
is finished. The expression *p++
,
equivalent to (*p)++
,
evaluates to the dereferenced p
first,
then increments the pointer p
(see section 2.4 for operator precedence and associativity
rules).
The function putchar_lcd()
places
a single character corresponding to its argument on the LCD. Its
prototype is
int putchar_lcd(int c);
For example, calls to putchar_lcd()
might
be
= putchar_lcd('m'); // put and assign 'm' to ch
ch ('\n'); // new line, ignore return value putchar_lcd
Background: special characters for the display
ASCII control characters can be used in similar ways to control many ASCII display devices. Each display make/model may interpret some ASCII control characters differently, but many will function similarly. Control characters can be sent to a display to perform specific tasks, such as clearing the display, starting a new line, and moving the cursor.
Special characters called escape
sequences correspond to ASCII character codes. These allow us to
write more descriptive code because it is easier to interpret the
meaning of the C escape sequences than the ASCII character codes. In our
display drivers, we will use a few escape sequences, but
translate them in the low-level function putchar_lcd()
for
the specific target display. For now, we must only know the function of
each sequence that we will later program into putchar_lcd()
. These
are shown in table 1.12.
Laboratory exploration and evaluation
Before you write your double_in()
and
printf_lcd()
functions, write a main program that will test it by calling it twice
from the main()
function,
assigning each result to a different variable. Then, as a check, print
the values of both variables on the LCD with printf_lcd()
, and on
the console using printf()
. See for main()
pseudocode.
Because the T1 C library already includes double_in()
and
printf_lcd()
functions, you can fully write main()
before double_in()
. The
standard C library header files stdio.h
and stdarg.h
and the T1
library header file T1.h
should be #include
d. The
latter provides the double_in()
and
printf_lcd()
functions, which will be overwritten by your own in problem L1.2, problem L1.3.
TODO explain
Reference the listing: lst. ¿lst:lab-1-main?
Reference a line number: lab1:nifpga_status
Write and debug the double_in()
function
as described in subsection L1.2. The definition of the
function in the main.c file will supersede the
library version.
Write and debug the printf_lcd()
function as described in subsection L1.3. The
definition of the function in the main.c file
will supersede the library version.
When writing double_in()
in problem L1.2, strchr()
and strrchr()
were used
to find multiple decimal points. Alternatively, the C function strstr()
could be
used. What assumption is being made if we choose to detect extra decimal
points with strstr()
and ".."
as
the second argument? Is this a valid assumption? Write a program using
strstr()
that
correctly identifies the extra decimal point without looping
through each character. Use the string "1.2.3"
as
a test case.
The assumption being made is that an extra decimal point will occur
next to an original. Although this is probably the most common case, it
is not a valid assumption, in general, because an entry like "1.2.3"
would not be detected.
An extra decimal point can be detected by invoking strstr()
twice:
first to locate a first occurrence and—if there is one—again, this time
on the remaining part of the string, to detect a second.
Online Resources for Section L1
No online resources.