Lab 5
Digital Stopwatch
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
In this lab exercise you will use your Arduino board and the LCD shield to implement the functionality of a digital stopwatch. This lab will incorporate embedded coding concepts from multiple I/O modules including timers and digital I/O in a single project. You may utilize code from other labs or in class demos as modules/functions for this project.
To see a short video demonstrating the operation of lab, click here.
For a copy of the Lab 5 grading sheet, click here.
Stopwatch Application
The Arduino and LCD shield will be used to implement a stopwatch application that counts upwards in increments of tenths of seconds from 0.0 to 59.9 seconds. It will provide the ability to start, stop, and reset the stopwatch back to 0.0. It should also implement a “lap” feature which freezes the displayed time at the instant the “lap” button is pressed while still keeping the internal watch time incrementing. When “lap” is pressed again (or “start” is pressed again) the internal watch time (which has been running) should be re-displayed and then continue as normal.
Note, while we can use delay functions to cause things to happen every 0.1
seconds, this approach will not give us good results in this lab for several
reasons. Recall that when using delay functions the time is measured only while
the function is executing and it will miss the time associated with the code
checking for button presses or updating the LCD. From previous labs we know
that the time we miss this way accounts for for several milliseconds each
iteration of the loop which adds up to seconds when measuring durations close
to a minute. The alternative approach we will be using in this lab is to use a
hardware timer that runs in parallel to our while (1)
loop code that
continuously measures the time. To get updates from the hardware timer at
precise time intervals, we will enable the timer interrupts that will call our
interrupt routine, a special C function.
Button Inputs
As input to your stopwatch you will use two push-buttons on your breadboard.
-
The button connected to port PC2 button will serve as your “Start_Stop” button. When pressed it simply toggles the state of the stopwatch between paused and running.
-
The button connected to port PC4 will serve as the “Lap_Reset” button.
-
When the stopwatch has been started and is running, a press of “Lap_Reset” will serve as the lap feature and will freeze the display while keeping the internal time incrementing. Any subsequent press of either “Start_Stop” or “Lap_Reset” will toggle the lap feature to return to the running display of the current time.
-
When the stopwatch is paused, a press of “Lap_Reset” will reset the time to 0.0.
-
Below is a schematic diagram of the circuit for Lab 5.
State Machine Approach
It is recommended that the stopwatch be implemented as a state machine with three states: PAUSE, RUN and LAPPED. Presses of the button will cause updates/transitions between states as shown below and may cause actions to be taken depending on the transition that occurred. What needs be done in each state (whether the time is incremented and what is displayed) should be fairly intuitive. The program will also need some sort of internal representation of the time (more on that below) and logic to decide when to update the display.
Getting Started
To get started, create a “lab5” folder, download the lab5.zip file from the class web site and put it in the “lab5” folder. The zip files contains a lab5.c file to use for the program and the Lab5_Answers.txt file for the review questions. Copy your Makefile, lcd.c and lcd.h files from the lab4 folder to the new lab5 folder.
The lab5.c template file should be used as a starting point for the program. Add code to implement the state machine described above with the three states. The main loop of the program is performing three tasks each time around the loop:
-
Checks the two buttons for presses
-
Depending on the current state and the button inputs, may take some action and/or change to a different state.
-
If the stopwatch’s time has changed, update the display to shown the new time.
While that loop is running, your timer module is also running generating interrupts every 0.1 second. The main loop and the ISR can be thought of as two separate programs, both running at the same time, that communicate about what needs to happen through the state variable and flag variables. For example, the code in the timer ISR can set a flag variable to indicate that the internal time has changed due to the ISR being invoked. The main program can check this variable, and the state variable, to determine whether the displayed time on the LCD needs to be updated.
Displaying Text Strings on the LCD
In many of the EE109 labs we will have to display text of some type on the
screen of the LCD. In the previous lab we used the lcd_writedata
function to
display a single character on the LCD, but it would be better if we could
display an entire string of charaction with just one function call, and had the
ability to display the strings anywhere on the screen. In this section we will
learn how to do that.
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 collection 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:
-
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.
-
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. The zero byte marks the end of the characters in the string.
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 character (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:
- Looks at a value in the string (a character)
- If character is the NULL character (i.e. the value of the character variable is zero) that is at the end of all C strings, all the characters in the string have been sent to the LCD so the loop is done and the function returns.
- Otherwise, use the
lcd_writedata
function to send the character to the LCD. - Move to the next element in the string.
We can pass to the lcd_stringout
function an array of ASCII codes either by
our first filling an array in one of the ways shown above, or by just putting
the string in the function call as the argument. For example to print our name
we just need to do
lcd_stringout("Tommy Trojan");
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 the cursor happens to be sitting. To be able to write anywhere on
the screen we need to use another function to tell the LCD where the string
should go.
Moving the Cursor on the LCD
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. It just keeps writing characters on that
line even if some go past the right side and can’t be seen. If you want the
additional characters to go on the second line, you have to use lcd_moveto
to
position the cursor there before writing the additional characters.
Task 1: Write the Splash Screen
All EE109 labs from now on should have splash screen that shows your name and the which lab it is.
-
In the
lab5.c
file, add code to iniitialize the LCD module by calling thelcd_init
function. -
In the section of the file that says to add the splash screen, use the
lcd_moveto
function to move to a position on the first line and then uselcd_stringout
to display your name, or as much as will fit. If your name is less than 16 characters, try to center it on the line by adjusting thecolumn
argument to thelcd_moveto
call. -
Move to the second line and display the name of the lab assignment.
-
Add a call to the
delay_ms
function to delay the program for one second or more so people have time to read the splash screen. -
Call
lcd_writecommand
with an argument of 1 to clear the screen so parts of the splash screen are not left on the display while the rest of the program runs.
Task 2: Configure the Timer Module
The counting action of the stopwatch is based on using the 16-bit TIMER1 module to generate an interrupt every 0.1 seconds. Refer to the slides shown in the introductory lab video for information on the various register bits settings appropriate for this application.
Important: The information provided in the slides and video about setting up the timer were for generating interrupts every 0.25 seconds. That was just an example of configuring the timer and is not the timer interrupt interval you are using in this lab.
Values for the timer prescaler and the modulus must be selected that yield a 0.1 second timer interrupt interval.
-
The prescalar is controlled by the three bits, CS12, CS11 and CS10 bits, in the TCCR1B register. It can be set to divide the 16MHz clock by 8, 32, 64, 128, 256 or 1024 depending on the state of these bits.
-
The modulus value is stored in the OCR1A register and can be any 16-bit value from 1 to 65535.
There may be more than one combination of prescalar and modulus that will yield the correct interval between interrupts. In that case you can use any pair that works.
Once the prescalar bits are set to a value that selects one of these divisors, the timer starts counting. Conversely, if you want to stop the timer at any point, setting the three prescalar selection bits to 000 turns the prescaler off and this stops the counting of the timer. The timer is effectively turned on and off by changing the prescaler settings in register TCCR1B.
Task 3: Test the Timer Module
Before trying to implement any timing functions we want to confirm that the TIMER1 module is generating interrupts at the correct 0.1 second interval. To do this add a couple lines of code to the ISR to cause a port bit to do transitions that can be observed with the oscilloscope. All you need is for the port bit to change at the each occurrence of the ISR.
Add the following line in the main()
routine before the start of the while
(1)
loop to make PC5 an output.
DDRC |= (1 << PC5);
Then add code to start TIMER1 using the prescalar bits you determined above.
TCCR1B |= ???
Before the main loop, call the function that enables interrupts.
sei();
Lastly, edit the ISR routine so it looks like the code below.
ISR(TIMER1_COMPA_vect) { PORTC ^= (1 << PC5); // Flip PC5 each time ISR is run }
The XOR operation on PC5 will cause it to flip to the other state each time the ISR is invoked.
Download the code to your Arduino and connect a scope probe to the PC5 pin to observe the signal. If the ISR is running every 0.1 seconds, this should create a squarewave signal on PC5 with a rising or falling edge every 0.1 seconds resulting in a signal frequency of 5 Hz.
Checkpoint: Show one of the teaching staff the 5 Hz test signal from PC5 on the scope to receive the checkpoint credit.
Task 4: Tracking the Time
It is strongly recommended that you first make your program simply count the time from 0.0 to 59.9 and then start over without dealing with the buttons. Essentially build a one minute duration clock. Until your program can do this there is no point in trying to make it respond to the the button inputs.
When the stopwatch is running, the TIMER1 module’s ISR routine will get called
every 0.1 second and the program must increment the stopwatch’s time value
whenever the ISR is called. There are numerous ways the program can store the
time value but some ways are better than others. It is recommended to not
store the time value as a single number, such as the number of seconds or
tenths of seconds that has passed since the timing started. While this makes
incrementing the time value very easy, it requires doing a lot of divisions or
calls to snprintf
to format the displayed number properly. It’s more
efficient to store the time as three separate fixed-point variables, one for
each of the three digits to be displayed, and have your ISR change these
numbers as needed to increment the time.
If you use three separate variables for the three digit, each time the ISR is invoked it increments the number of tenths of seconds to be displayed. If the tenths value goes above 9, reset it to zero and increment the number of seconds. When the seconds goes above 9 reset it to zero and increment the tens digit. If the tens digit goes above 5, reset it to 0 so the time rolls over to 0.00.
The time values can be stored in a couple of ways. If you store them as plain
numbers (1, 2, 3, etc.) then you will need to convert these to the ASCII
representations of the numbers before sending them to the LCD using the
lcd_writedata
function. The LCD only displays ASCII character codes.
Alternatively, you can store the time values as their ASCII number codes (0x31
for 1, 0x32 for 2, etc.). This makes incrementing and comparing them a bit
more difficult but no conversion is necessary when sending the number to the
LCD with your lcd_writedata
function.
The time display should not show any unneccesary leading zeros. For example, when the time is 7.5 seconds, it should just display “7.5”, not “07.5”. This means that sometimes there will be one digit to the left of the decimal point (0-9), and sometimes two digits (10-59). Regardless of how many digits are displayed, the decimal point should not move around on the display when the numbers to the left of the decimal point change from one digit to two digits. The decimal point should always be in the same position on the display.
Cool C Programming Trick: The integer values 0 through 9 are sequential, and so are their ASCII codes starting at the code value 0x30. ‘0’ = 0x30, ‘1’ = 0x31, … , ‘9’ = 0x39. So to convert an integer value of 0 through 9 to its ASCII code, just add 0x30.
lcd_writedata(x + 0x30);
Or even better, since ‘0’ = 0x30, just add the character ‘0’ to get the same result.
lcd_writedata(x + '0');
Task 5: Add the Stopwatch Functions
Once you are sure the program correctly increments and displays the time, you can then add code to read the button inputs and implement the state transitions.
Each time a button is pressed, the program must determine what to do based on the state the program is currently in. For example, if the Start_Stop button is pressed and the program in in the “RUN” state, it should transition to the “PAUSE” state and turn off the timer so no more TIMER1 interrupts occur.
Since the buttons are used for doing multiple state transitions
(e.g. PAUSE→RUN and RUN→PAUSE), it’s necessary to do button
debouncing as discussed in the Unit 4 lecture. If debouncing is not done, the
state machine will make multiple transitions for a single button press and it’s
impossible to know what state it will end up in. The lab5.c
provided
contains a template for a function that can be called to complete the
debouncing operation after a button press has been detected.
void debounce(uint8_t bit) { // Add code to debounce input "bit" of PINC // assuming we have sensed the start of a press. }
The “bit” argument to the function is the number of the input bit in the PINC register to debounce. Use the information in the Unit 4 slides to finish this routine. Note that this routine finishes the debouncing operation after other code has determined that the button has been pressed.
For each of the four transitions shown on the state diagram your program should
call this debounce
function with the proper argument after a button press has
been detected.
Your program should meet the following requirements:
-
On startup, show a splash screen for a couple of seconds with your name and the lab number.
-
Initialize the count to 0.0 whenever the program is started. The program should be in the “PAUSE” state on start up.
-
Start counting in tenths of seconds when “Start_Stop” is pressed and the timer is paused. Note: The time value should increment to 59.9 seconds and then on the next increment go back to 0.0 and continue incrementing from there.
-
Correctly display all times on the LCD.
-
Stop counting when “Start_Stop” is pressed while the timer is running.
-
Hold the displayed time while continuing internal time updates when “Lap_Reset” is pressed while the timer is running.
-
Update the display with the current internal time and continue counting when “Start_Stop” or “Lap_Reset” is pressed and the display time is being held (i.e. LAPPED state).
-
Reset the time to 0.0 when “Lap_Reset” is pressed and the timer is paused.
Task 6: Improving the Stopwatch
If the stopwatch is implemented as described above with three states (PAUSE, RUN and LAPPED) and debouncing used on all four transitions, it works but has a problem. When the stopwatch is paused, if you press and hold down the Start_Stop button button, the stopwatch will start timing as soon as the button is pressed, but the display is not updated with the advancing time until the button is released (Try it!). If you examine the code you wrote you can see this is because the program is stuck in the debouncing routine as long as the button is held down. The state machine is in the RUN state, and TIMER1 is causing the time to advance, but as long as the program is in the debouncing routine it can’t update the display. We would prefer to see the time start to advance immediately when the button is pressed, but still incorporate the debouncing.
To do this we will add a fourth state “STARTRUN” between the PAUSE and RUN states which can be thought of as a special state to perform the debouncing action but still allow the display to update.
When in the PAUSE state, as soon as the Start_Stop button is pressed we start the timer and then transition to the STARTRUN state without waiting for the button to be released. While in STARTRUN the program watches the Start_Stop button and stays in STARTRUN until that button is released. Note the difference of this method. Before we stayed in the debouncing code until the button was release, while now we just stay in the STARTRUN state (not stuck in the debouncing loop) until the button is released. The effect of this STARTRUN state is that it does the debouncing, but still allows the main loop of the program to run and the display can be updated.
By adding the new state we eliminate the while loop used for debouncing the button, but we still keep the 5ms delays when transitioning in and our of the STARTRUN state. These delays are needed to allow the mechanical switches to stabilize in the open or closed state. For more details on using the delay for debouncing see slide 4.57.
Add the STARTRUN state to your code and use it to do the debouncing between the PAUSE and RUN states. The debouncing code you had before can be left unchanged for the other three transitions (RUN→PAUSE, RUN→LAPPED and LAPPED→RUN). Once this new state has been incorporated into the code, you should be able to show that pressing Start_Stop when in the PAUSE state immediately starts the display updating.
Results
When your program is running you should be able to confirm the following
-
Display your splash screen.
-
Count initializes to 0.0 seconds
-
Pressing the Start_Stop button starts and stops the timing. Should be able to do this multiple times without the display returning to 0.
-
The timing rate appears to be correct, not too fast or too slow.
-
When timing, pressing the Lab_Reset button freezes the display while the timing continues internally. When a button is next pressed the display updates to the correct time.
-
If the timing has been paused, pressing the Lab_Reset button resets the display to 0 seconds.
-
When paused, if Start_Stop is pressed the display starts showing the advancing time immediately and does not freeze if the button is held down.
Reminder: Any variable that needs to be accessed from both the main program
and the ISR must be declared as a global variable at the top of the program
(not in the main routine). In addition these variables should be declared with
the “volatile
” keyword to tell the compiler that their contents can change
outside the main stream of code execution.
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.
-
Review the conceptual operation of a timer interrupt.
- For a 16-bit timer with clock frequency of 16MHz (like the Arduino) and prescalar of 64, how much time corresponds to a timer value of 2000?
- With a clock frequency of 16MHz and prescalar of 8, what is the longest amount of time we can track with an 8-bit hardware timer.
-
The Timer modules we have used also have the ability to make an output pin turn ON (set), OFF (clear), or Toggle when the timer reaches the OCR1A or OCR1B values (i.e. the hardware can automatically control the output value of a pin). By searching the data sheet (ATmega328P datasheet is linked on our website from the Resources page) answer the following question:
- TIMER1 (the 16-bit timer) can control the pins that are associated with OC1A and OC1B signals. Find to what pins these signals are wired by looking at Figure 1.1 ("28 PDIP" package diagram) on page 12 of the data sheet, or alternatively, Tables 14-3, 14-6, or 14-9.
- In this lab we use TIMER1 in the ``Clear Timer on Compare'' or CTC mode. In this mode when the counter reaches the value in OCR1A register it generates an interrupt and starts counting again from zero. Using the information in section 16.11.1 and table 16-1, describe what the OC1A and OC1B pins would do when the timer reaches the OCR1A value if during initialization we used the statement `TCCR1A = 0x60;`