EE109 – Spring 2017: Introduction to Embedded Systems
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, ADC, 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.
The Arduino and LCD shield will be used to implement a stopwatch application that counts upwards in increments of tenths of seconds from 00.0 to 59.9 seconds. It will provide the ability to start, stop, and reset the stopwatch back to 00.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.
As input to your stopwatch you will use two of the push-buttons (Up and Down) on your LCD shield.
- The Up button will serve as your "Start_Stop" button. When pressed it simply toggles the state of the stopwatch unless in lap mode (more to come ...).
- The Down button 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 stopped, a press of "Lap_Reset" will reset the time to 00.0.
The code from your LCD lab can be reused here to perform the ADC conversion on the button input value and determine if a button was pressed.
State Machine Approach
It will likely be beneficial to keep a "state" or mode variable tracking what mode your stopwatch is in. Presses of the button will cause updates/transitions between modes or 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.
In terms of overall structure, there are many ways to organize your program. One suggested approach is to have a loop in the
main() routine that polls the ADC for button presses and updates the state of the stopwatch as needed. While that loop is running, your timer is also running generating interrupts every 0.1 second. You can put your display and time update logic in the timer ISR. That code can examine the state variable to determine if an update of the internal and/or displayed time is necessary. 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 variables.
Note 1: Since the display can really update only every 0.1 second it would make sense to put all the display logic (except for maybe the reset case) into the code that gets executed every 0.1 second, which is the ISR for the TIMER rather than putting the display logic into the main() loop.
Task 1: Using Multiple Source Code Files
This lab will use the LCD functions that you developed in Lab 6. However since these routines are tested and working, we want to separate them from the rest of the Lab 7 code that is being developed. Most software projects larger than a few hundred lines of code are split in to multiple source code files, and we want to learn how to properly do that in this lab.
- Create a lab7 folder in your ee109 folder.
- Make a copy of your lab6.c file that contains all the LCD functions. Move it to the lab7 folder and rename the file to be "lab7.c".
- Now duplicate the lab7.c file and rename the copy to "lcd.c". At this point you should have two files in the folder: lab7.c and lcd.c
- Edit the
lcd.cfile and remove all the variable declarations and code for the Lab 6 stuff, but leave in
- The #include lines for avr/io.h and util/delay.h.
- The LCD functions prototypes at the top of the file.
- The functions init_lcd, moveto, stringout, writedata, writecommand, and writenibble.
- Create a new file in the lab7 folder called "lcd.h" and into that file copy the function prototypes from lcd.c for all the functions just listed EXCEPT for the prototype for writenibble. The file should have just 5 lines in it.
- Go back to lcd.c and replace the five function prototype lines you copied to lcd.h with #include "lcd.h". The top of the lcd.c file should look something like this.
#include <avr/io.h> #include <util/delay.h> #include "lcd.h" void writenibble(unsigned char);
The reason the prototype for writenibble is not included in the lcd.h file is that that function is only used by the other functions in the lcd.c file, so there is no reason for it to be known by the rest of the program. Think of it as a private function that can only be used by the other functions in lcd.c.
The #include line for the lcd.h must use double quotes instead of the '<' and '>' characters. The angle brackets tell the compiler to look for the header file in the development software directories and are always used for including header files that are part of the development software. The double quotes tell it to look for it in the local directory are should be used with header files you create.
- Edit the lab7.c file and remove all the LCD functions (both the code and the function prototypes).
- Near the top of the file after the other #include lines, add the line
This is the same line you put near the top of the lcd.c file and it will read in the five function prototypes from the lcd.h file.
- Put a copy of the Makefile from Lab 6 in the lab7 folder.
- Edit the Makefile to change the OBJECTS line to be correct for this lab but also add the name of the lcd.o file to this line. Since your source code is now in two files, both of them must be listed as object files in the Makefile. For this lab the OBJECTS line should look like this.
OBJECTS = lab7.o lcd.o
When you do a "make", both source code files will be compiled (if necessary) and linked together to create the executable program.
Task 2: 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 class for information on the various register bits settings appropriate for this application.
Values for the timer prescaler and the modulus must be selected that yield a 0.1 second timer interrupt interval. The modulus value is stored in the OCR1A register can be any 16-bit value from 1 to 65535. 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 1, 8, 64, 256 or 1024 depending on the state of these bits. Once these bits are set to a value that selects one of these divisors, the timer start 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.
Find a combination of prescalar setting and counter modulus for TIMER1 that will cause it to generate interrupts every 0.1 seconds. From this determine which values will be used for the CS12, CS11 and CS10 bits in register TCCR1B. Fill in your choice on the Lab 7 grading rubric.
Tracking the Time
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.
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
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
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.
writedata(x + 0x30);
Or even better, since '0' = 0x30, just add the character '0' to get the same result.
writedata(x + '0');
Task 3: A Running Timer
Before writing the full stopwatch lab with buttons to control the stopwatch timer, first just implement a running timer. When the program starts, the time shown on the LCD starts at 00.0 and then increments every 0.1 seconds. When it reaches 59.9 it rolls back to 00.0.
Use/alter code from your previous labs/exercises as a basis for completing this task. You will need to integrate code from the timer examples to make a timer that generates an interrupt every 0.1s, code to output to the LCD, and code for the main routine. ADC code from the LCD lab to poll the button inputs is not used in this task but will be used in the next.
Below are some guidelines for getting started.
- Initialization code will be necessary for the various modules being used: ADC, LCD, and TIMER1. You MUST have separate initialization functions for each of these devices (e.g.
init_lcd, etc.) and consider what appropriate arguments could be used with each initialization function (if any) that allows you to reuse these functions in a future project. Important Tip: We recommend putting the I/O port initialization (things like the PORT and DDR initialization) for things like the LCD into the
init_lcdfunction itself (if you haven't already). But be careful, ensure that your initialization code ONLY affects the bits used by that device and not other bits of the PORT. This is important because in your project or future labs when you have many more devices interfaced to your microcontroller, calling
init_lcdshould not accidentally set some other PORT bit to an output, etc.
- Define a global variable char array to hold the time, 'time', or use three separate variables for tens of seconds, seconds and tenths of seconds. Use these to store the appropriate time data.
- Write the TIMER1 ISR. It should modify the three time variables as needed. If the count is at 59.9, it should roll over to 00.0. Once the new time has been determined, the ISR should update the LCD display with this information.
- At the start of the main routine initialize the LCD and TIMER1 module.
- Show a splash screen for a couple of seconds as in Lab 6. You can decide what to put on the screen during this time.
- Start TIMER1 by setting the three prescalar bits to the values you determined above to generate interrupts every 0.1 second.
- Once you have started the timer, your program can enter a loop but this loop doesn't have to do anything (for this task). The program just spends its time looping waiting for the next interrupt to occur. All the work of responding to the interrupts, changing the three time variables, and updating the LCD display should be done in the TIMER1 ISR.
Once you have the running timer working check that it operating correctly. The time display should advance at the correct rate, the numbers should change properly and the time should roll over to 00.0 after reaching 59.9. Don't trying adding the stopwatch buttons to the program as described below until you have this task completed.
Task 4: The Stopwatch Program
Your running timer program should now be expanded to add the buttons for implementing the stopwatch functions.
- Add a global variable "state" to hold the state information. Decide what values you will use to represent each of the three states.
- Add code to call the ADC initialization routine.
- In the main loop use code from Lab 6 to check for button presses and make changes to the "state" variable as needed.
- Make changes to the TIMER1 ISR to use the information in the "state" variable to determine whether or not to update the LCD display with the current time.
- Think carefully about what variables should be declared with the "volatile" modifier. Any global variable that will be modified by an ISR and used in any other routine should be declared as volatile.
Your program should meet the following requirements.
- On startup, show a splash screen for a couple of seconds as in Lab 6.
- Initialize the count to 00.0 whenever the program is started.
- Correctly display all times on the LCD.
- When the timer is stopped, start counting in tenths of seconds when "Start_Stop" is pressed. Note: The time value should increment to 59.9 seconds and then on the next increment go back to 00.0 and continue incrementing from there.
- When the timer is running, stop counting in tenths of seconds when "Start_Stop" is pressed.
- When the timer is running, and "Lap_Reset" is pressed, hold the displayed time while continuing internal time updates.
- 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. LAP state).
- Reset the time to 00.0 when "Lap_Reset" is pressed and the timer is stopped.
Make sure to comment your code with enough information to convey your approach and intentions. Try to organize your code in a coherent fashion. Once you have the assignment working demonstrate it to one of the instructors and get their initials on your rubric. Turn in a copy of your source code through the link on the web site.