EE109 – Fall 2022 Introduction to Embedded Systems

EE109 – Fall 2022: Introduction to Embedded Systems

Lab 5

State Machines

Honor Statement

This is an individual assignment. The code you write, use for the demo and submit should be wholly your own and not produced by working in teams or using, in part or in whole, code written by someone else (found online, from a fellow student, acquaintance, etc.). You will be asked to verify this when you submit your code on Vocareum by signing your name in the honor.txt file on Vocareum. Penalties for violation include a 0 on the assignment and a potential further deduction or referral to Student Judicial Affairs.

Introduction

This lab exercise is an extension of Lab 4 where you developed some of the software to interface your Arduino with an LCD display. In this lab you will learn how to use C functions to display short messages on the LCD, and use the state machine technique to write a combination lock program.

For a copy of the Lab 5 grading sheet, click here.

Getting Started

  1. Create a "lab5" folder in your "ee109" folder.
  2. Download the "lab5.zip" file from class web site and extract the "lab5.c" and "Lab5_Answers.txt" files from it.
  3. From the "lab4" folder, copy the "Makefile", "lcd.c" and "lcd.h" files to the "lab5" folder.
  4. Edit the Makefile to change the OBJECT line to say "lab5.o" instead of "lab4.o".

Displaying characters

The LCD display uses the ASCII character codes to determine which characters to display. Any byte written into the LCD's data register will result in the character with that ASCII code being displayed on the LCD and the cursor that indicates where the next character will go is advanced one position to the right. For example if the bytes with values 0x55, 0x53, 0x43 are written to the data register one after the other, the characters "USC" will appear on the screen.

In order to write characters to various places on the display, we need to ability to move the cursor to one of the 32 character positions prior to sending the ASCII data. Each character position on the display has a unique address in the same way that bytes in computer's memory have unique addresses. The "lcd.c" file contains the function lcd_moveto that accepts as arguments a row (0 or 1) and column number (0 to 15) and translates that to the command to move the cursor to the position with the correct address. For example, to move the the fifth column (column 4) on the second row (row 1), we would use this command.

lcd_moveto(1, 4);

The lcd_moveto function is designed to hide from the user the ugly details about how the LCD actually addresses the characters in the display but instead just uses a row number (0 or 1) and a column number (0 to 15).

Once the cursor has been moved to a position, any subsequent data bytes sent will cause characters to appear starting at this position and extending to the right. Note: if you write more characters than will fit on a line, it doesn't automatically wrap to the next line. If you want the additional character to go on the second line, you have to use lcd_moveto to position the cursor there before writing the additional characters.

Character strings in C

The lcd.c file contains a function, lcd_stringout, that will write to the LCD a string of multiple ASCII characters starting at wherever the LCD cursor is located. In the C language these are often called "C strings" and are simply a "char" array containing the characters to be displayed.

It is very important that this array be a proper and correct collecction of characters or garbage is likely to end up being displayed on the LCD. A array that holds a string of characters in C has to

  1. Contain any number of printable ASCII character codes. The code for the first character in the string is in array location 0, the next in location 1, etc.
  2. Contain a zero byte (also called a "NULL" byte) in the next array location after the last ASCII character to be display. This is just a byte is containing the value zero.

There are many ways to create a C string. For example to make a string containing "USC" we could do this.

char school[4] = "USC";

The compiler puts the ASCII code for 'U' (0x55) in school[0], the code for 'S' (0x53) in school[1], the code for 'C' (0x43) in school[2] and puts a zero byte in school[3]. It's very important that the array was declared as size 4 even though we only had 3 characters to put in it since the compiler needs the extra place in the array for the zero byte.

We could have also assigned the array values individually.

char school[4];

school[0] = 'U';
school[1] = 'S';
school[2] = 'C';
school[3] = 0;

The last line could also have been written as

school[3] = '\0';

but it would NOT be correct to write the last line as

school[3] = '0';

since this assigns school[3] to be the ASCII value of 0 (0x30).

No matter how the C string is created, it is very important that it is a correctly formatted array with the zero byte at the end. If the array elements do not contain proper ASCII codes, or the zero byte is missing, all sorts of strange things will appear on the LCD when it is sent to the LCD, and the Arduino's program will probably die at that point.

Displaying strings of characters

To write a string of characters to the LCD, the "lcd.c" file contains the the function lcd_stringout. This routine writes a string of multiple characters, as described above, to the LCD starting at the current cursor position. The argument to lcd_stringout must be pointer to a standard C string, an array of "char" variables each containing an ASCII character code, terminated by a zero byte. Normally in programs we just write the name of the array containing the C string as the function argument and the compiler knows to pass a pointer to that string when the function is called.

Open up the "lcd.c" file and look at how the lcd_stringout function works. It contains a loop that does the following:

Keep in mind that the lcd_stringout function has no idea where it is writing characters on the screen. It just writes characters at whatever place on the screen that you moved the cursor to with the lcd_moveto function. It doesn't complicate things by trying to wrap text onto another line if it goes past the right side of the LCD. It just writes all the characters in the string, one after another, even if some go past the right side and can't be seen.

Displaying binary data

The lcd_stringout function will display a C string of characters on the LCD, but what about displaying the value of variable that contains some binary value? Many people try doing the following only to find out that it doesn't work.

int x = 1234;

lcd_stringout(x);

The "x" variable contains the value 1234. It's not the name of an array containing the ASCII codes for '1','2', '3', '4' and then a zero byte. If we try the above code the LCD will end up with either nothing or garbage displayed on it, and the Arduino's program may stop running.

It's important to remember that the LCD only displays ASCII characters. Any other type of value must somehow be converted into an array of ASCII characters, a C string, before it can be displayed. In the section below we will discuss the snprintf function which is used to convert binary variables to strings of ASCII codes.

Creating character strings

Since the LCD is used to display strings of characters, it's necessary to have a efficient way to create the strings to be displayed. Constant strings are no problem and can be given as an argument to the lcd_stringout function.

lcd_stringout("Hello, world");

It gets more complicated when you need to create a string containing variable values, such as numbers. For example, in C++ when you write

cout << "Answer is " << x << endl;

cout is converting the numeric value of x into the ASCII representation of that number.

In C, the best tool for creating character strings is the snprintf function that is a standard part of most C languages. Important: In order use snprintf, or any other routine from the C standard I/O library, you must have the following line at the top of the program with the other #include statements.

#include <stdio.h>

The snprintf function is called in the following manner

snprintf(buffer, size, format, arg1, arg2, arg3, ...);

A quick example is:

char buf[17];
int x = /* some value */
snprintf(buf, 17, "Answer is %d", x);
lcd_stringout(buf);

The arguments for snprintf are:

buffer

The destination char array large enough to hold the resulting string including the NULL byte that has to be at the end. For example, if you want to store the string "Hello" in a array, the array must be at least 6 elements long.

size

The size of the destination char array. This tells the snprintf program the maximum number of character it can put in buffer regardless of how many you try to make it do. Keep in mind that the snprintf functions always puts a NULL byte at the end of the string it creates. If you ask it to put "12345" in an array of length five, you will end up with the string "1234".

format

The heart of the snprintf function is a character string containing formatting codes that tell the function exactly how you want the following arguments to be formatted in the output string. More on this below.

arguments

After the format argument comes zero or more variables containing the values to be placed in the string according to the formatting codes that appear in the format argument. For every formatting code that appears in the format argument, there must be a corresponding argument containing the value to be formatted.

The format argument tells snprintf how to format the output string and has a vast number of different formatting codes that can be used. The codes all start with a percent sign and for now we will only be working with two of them (and a simple variation of one of them):

%d
Used to format decimal integer numbers. When this appears in the format string, the corresponding argument will be formatted as an decimal integer number and placed in the output string. It will only place as many characters in the buffer as needed to display the number. A number from 10 to 99 will take up two places, a number from 100 to 999 will take up three places, etc. A useful variation of this is to specify a minimum field width by using the form "%nd" where the 'n' is the minimum number of spaces the converted number should occupy. If it takes less space than 'n' characters it will be right justified in the 'n' character field with spaces to the left. For example, a format of "%4d" will print an argument of 65 as "   65" with the number right justified and two leading spaces.
%s
Used to format string variables. When this appears in the format string, the corresponding argument will be assumed to be a string variable and the whole string variable will be copied into the output string.

The format string must have the same number of formatting codes as there are arguments that follow the format argument in the function call. Each formatting code tells how to format its corresponding argument. The first code tells how to format "arg1", the second code is for "arg2", etc. Anything in the format argument that is not a formatting code will be copied verbatim from the format string to the output string.

Example: Assume you have three unsigned char variables containing the month, day and year values, and a string variable containing the day of the week, and you want to create a string of the form "Date is month/day/year = dayofweek".

char date[30];
unsigned char month, day, year;
char *dayofweek = "Wednesday";
month = 2
day = 14;
year = 18;

snprintf(date, 30, "Date is %d/%d/%d = %s", month, day, year, dayofweek);

After the function has executed, the array "date" will contain

Date is 2/14/18 = Wednesday

Since you told snprintf that the size of the array was 30, the maximum number of characters it will put in the array is 29 since as with all C strings, the string has to be terminated with a null byte (0x00).

An additional example is shown here:

char x = 10, char y = 12;
char z = x - y;
char buf[12]; // make sure you add one spot for the null character
snprintf(buf, 12, "%d-%d=%d", x, y, z); // yields "10-12=-2" but not on the LCD
lcd_stringout(buf); // prints "10-12=-2" on the LCD

Beautifying Your Output

Often times we may want a number to take up a fixed number of spaces on the screen. For example if we print out 9 and then 10 the fact that 10 grew into a 2nd digit may make the visual layout look sloppy. We might want all our numbers to use 2 digits (if we know in advance that they will be in the range 00-99) or 3 digits (if we know the range will be 0-255). To do this we can add modifiers to the %d format string modifier.

We can also set the string to use 0-padding fill in blank spaces with 0s.

A good reference for format strings can be found here.

Task 1: Write some strings to the screen

We now have all the tools necessary to write something to the LCD screen. In this task, we want you to practice creating strings that contain variable values using snprintf(). To do this, you should:

  1. Near the top of lab5.c add the call to lcd_init to initialize the LCD display.
  2. Add code to write two lines of text to the screen, before the while(1) loop which should have no code it at this time. The first line on the LCD should display your name (as much as will fit) to the first row. The second row should contain a string with your birthday that was created using snprintf, similar to the way the string above was formatted with today's date using numerical values and the "%d" format strings. It should be displayed somewhat centered on the bottom row and not just start at the 0-th column of the row. This ensures you understand how to use the lcd_moveto function. After both rows are displayed on the LCD, make the program delay for 1 second.
      /* Call lcd_stringout to print out your name */
    
      /* Use snprintf to create a string with your birthdate */
    
      /* Use lcd_moveto to start at an appropriate column 
         in the bottom row to appear centered */
    
      /* Use lcd_stringout to print the birthdate string */
    
      /* Delay 1 second */
    
      while(1)
      { 
      }
    

Task 2: Build a combination lock

For this part of the lab you will build a simple combination lock that uses two buttons (A and B) for inputting the lock combination. When the correct sequence of five A's or B's has been entered, the LCD panel will show a message that the device is now unlocked. As the buttons are pressed the LCD panel will show the sequence of entries that have been correctly done to this point. If at any time a button has not been pressed for 3 seconds, the lock returns to the initial state.

To see a short video demonstrating the operation of this lab, click here.

The lock operation is based on a state machine where there is a state for each number of correct inputs that have been entered to this point. Since there are five inputs required to open the lock, the state machine will have six states for 0 through 5 correct entries.

The diagram above shows the six states but not the transitions between them caused by pressing the buttons. It will be your job to determine the transitions from each state based on whether the A or B button has been pressed. Indicated on the state diagram are the transitions that occur if no button is pressed for 3 seconds. If that happens the state machine transitions to the initial state S0.

The initial state for the machine is state S0. The correct unlocking sequence is "B-A-B-B-A" and as this sequence is entered the machine transitions from S0 to S1 to S2 etc. until reaching state S5. When in state S5, the "Unlock" output is true, which is shown by a message on the screen. If an incorrect button is pushed at any time, the machine does not necessarily transition all the way back to state S0. Instead it should transition to the state that is correct for whatever has been entered so far. When in state S5 (unlocked) pressing either the A or B button resets the lock back to the state S0 where no correct entries have been made.

The Circuit

The circuit for this lab as shown below, is the same as in Lab 4 with two pushbuttons connected to Port C, bits 2 and 4. The 'A' button is connected to Port C, bit 2, and the 'B' button is connected to Port C, bit 4.

The Program

At the start of main, perform the appropriate initialization of inputs and the LCD screen and declare other variables you will need (i.e. state, etc.).

Leave in lab5.c the code you put in for Task 1 to write the two lines of text. We will use this as a "splash screen" for the combination lock program. The first line should show your name, but change the second line from showing your birthday to instead print something like "EE109 Lab 5". The delay of 1 second will allow us to see the splash screen, and then your code after that should clear the screen and begin the combination lock portion of the program.

The main part of the program is done inside the "while (1)" loop. Use a state machine implementation structure as discussed in class to do the following on each:

  1. Read the two input buttons and determine if either is pressed. In this part of the program you will have to implement some sort of button debouncing as we described in the lectures. You may assume the user will NOT press both buttons at once (though an interesting exercise would be to try to detect that occurrence, output some kind of error message to the LCD, and return to state 0 so that the user can not breach the lock by holding both buttons).
  2. If neither button is pressed just continue to the next iteration of the while loop and check again.
  3. Otherwise, if some button is pressed, use a set of if-else if statements or the switch statement to determine what state you are in, perform any state transitions, and update the variables that should be displayed.
  4. Display any new information on the LCD. To avoid repetitive copies of code in doing this, we will factor out the update of the display to the very end of our if-else if statements (see the hint below). Your program should not be rewriting the information on the screen if nothing has changed since this make the display flicker. Only update the LCD display if the state has changed.
  5. Put a delay of some small amount at the end of the loop so you know how long (roughly) that it takes to go around the loop. Your program can then calculate how long it has been since the last button was pressed by counting how many times it has gone around the main loop since a button was pressed. Once 3 seconds has passed, it should return to state S0 and the display should reflect this.

Important: Your program must ensure that no more than one state change occurs each time a button is pressed. For example, if the device is in S2 (B-A entered), pressing button B for a long time should not cause the device to go to S3 and then S4. Each press and release of a button can cause just a single state transition.

The program must display the sequence of buttons pushes that have been entered so far, and the status, locked or unlocked, of the lock. You don't have to present this information exactly the way is done on the lock demonstrated in the video. As long as the input sequence and lock status is displayed that's sufficient.

Hints:

Results

When your program is running you should be able to show that the lock responds correctly to both a correct and incorrect input sequence. For example

The answers to the review questions below should be edited into the Lab5_Answers.txt file. The Lab5_Answers.txt file and all source code (lab5.c, lcd.c, lcd.h and Makefile) must be uploaded to the Vocareum web site by the due date. See the Assignments page of the class web site for a link for uploading.

Please make sure to save all the parts used in this lab in your project box. These will be needed for labs throughout the rest of the semester. We suggest also saving all the pieces of wire you cut and stripped since those will be useful in later labs.

Review Questions

Answer the following questions and submit them in a separate text file ("Lab5_Answers.txt") along with your source code.

  1. To ensure a date appears in the format: MM/DD, what snprintf() function could be used to ensure dates like 6/9 are generated in a character buffer as 06/09
    char buf[17];
    int m = 6; d = 9;
    snprintf(buf, 17, /* you write the format string */, m, d);
    
  2. Bobby Bruin wants to add a secret code to the lock where the user can go directly from state S0 to the unlocked state S5 by pressing both buttons at the same time. Tammy Trojan, thinking about the execution speed of the processor, says this can't be done, at least not reliably. What is her reason for thinking so?