EE109 – Fall 2022 Introduction to Embedded Systems

EE109 – Fall 2022: Introduction to Embedded Systems

Lab 8

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, 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.

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

For a copy of the Lab 8 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 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.

Button Inputs

As input to your stopwatch you will use two of the push-buttons (Up and Down) on your LCD shield.

The LCD's buttons are interfaced to your Arduino differently than the individual buttons we have used in previous labs. Learning how to work with these buttons is part of Task 1 described below.

State Machine Approach

It is recommended that the stopwatch be implemented as a state machine with three states: Stopped, Started 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

There is no template file for this lab. It is suggested that you start with your Lab 6 files (lab6.c, adc.c, adc.h, lcd.c, lcd.h and Makefile) and copy them to a new lab8 folder and rename them as needed. Most of the code in the adc.c and lcd.c files can be used as is. Most of the changes will be in the lab8.c file.

As you edit the lab8.c file, make sure to remove any code or variable that are not needed for this lab, and to change any comments to reflect what your code is now doing for this lab.

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 does the following tasks:

While that loop is running, your timer module is also running generating interrupts every 0.1 second. The code in the timer ISR can examine the state variable to determine if an update of the internal and/or displayed time is necessary. If a display update is required, it sets the flag variable to tell the main loop to update the LCD the next time it checks the flag.

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.

The main loop can run at full speed with no delays if no buttons have been pressed. That will make it most responsive to the buttons. However a delay to handle debouncing will be needed if a button has been pressed . A delay of 100ms to 200ms should be in the code but should only happen after a button has been pressed, not every time through the main loop.

Task 1: Determining the Button Values

Note: The following task is a repeat of what you were asked to do in Lab 6 to determine
the button values. If you collected the information in that lab you can use those results and do not have to repeat the task here.

The buttons on the LCD do not operate in the same way as the individual push buttons that we have been using in previous labs. Rather than having all the LCD buttons produce an individual digital signal, all five of them are instead interfaced through a multistage voltage divider creating a single analog signal. Depending on which button was pressed, or none, this circuit produces a different voltage between 0 and 5 volts, and this signal is attached to the ADC Channel 0 input to the Arduino. By using the ADC to convert this voltage to a number it's easy to determine if one of the five buttons was pressed. Your ADC code from Lab 6 can be reused here to perform the conversion of the button input voltage and determine if a button was pressed.

When a button is pressed the analog voltage on ADC channel 0 changes to a unique value that identifies the button that was pressed. However in order for your program to determine which button was pressed you have to know what the ADC output is for each button. The easiest way to do that is to write code to loop continuously reading the ADC result and writing the value to the LCD.

Use your ADC routines code from Lab 6 to give the program the ability to use the ADC to read the button's analog signal. In the while(1) loop of the main routine add code to do the following.

  1. Call adc_sample to take a sample of ADC channel 0 and convert it to a digital value.
  2. Use the snprintf routine to format the ADC result into a string of numerical characters that can be displayed. (e.g. something like snprintf(buf, 5, "%4d", adc_result);
  3. Use your lcd_moveto function to move the cursor to the first character position on the first row.
  4. Use your lcd_stringout function to print the string on the display

Once the program is running, try pressing each button and see what value is displayed. You can record the values shown for each of the buttons on the Lab 8 grading sheet. These values will be used in later tasks. You should note that the ADC conversion results may not always be exactly the same each time a button is pressed. Due to electrical noise and thermal effects it may return, for example, 124 one time, 125 another and 126 yet another time. Your code must take this into account when working with the ADC results.

Polling or Interrupts?

In Lab 6 we used polling to acquire the values from the ADC, and then in Lab 7 we learned how interrupts could be used to accomplish similar tasks, and often do it more efficiently. In this lab you can use either method to read the ADC and determine which button, if any, was pressed. It might be simpler to get your lab working using polling, and the see if you can convert it to using interrupts to read the button inputs.

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 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.

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 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.

Task 3: Tracking the Time

It is strongly recommended that you first make your program simply count the time from 00.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 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.

For example, each time the ISR is invoked it increment 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, and so forth for the other digits.

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.

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 4: 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 "Started" state, it should transition to the "Stopped" state and turn off the timer so no more Timer interrupts occur.

Your program should meet the following requirements:

  1. On startup, show a splash screen for a couple of seconds.
  2. Initialize the count to 00.0 whenever the program is started. The program should be in the "Stopped" state on start up.
  3. Correctly display all times on the LCD.
  4. Start counting in tenths of seconds when "Start_Stop" is pressed and the timer is stopped. 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.
  5. Stop counting in tenths of seconds when "Start_Stop" is pressed and the timer is started.
  6. Hold the displayed time while continuing internal time updates when "Lap_Reset" is pressed and the timer is started.
  7. 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).
  8. Reset the time to 00.0 when "Lap_Reset" is pressed and the timer is stopped.

Results

When your program is running you should be able to confirm the following

The answers to the review questions below should be edited into the Lab8_Answers.txt file. The Lab8_Answers.txt file and all source code (lab8.c, lcd.c, lcd.h, adc.c, adc.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 ("Lab8_Answers.txt") along with your source code.

  1. Review the conceptual operation of a timer interrupt.
    1. 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?
    2. 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.
  2. We want you to feel comfortable reading documentation to understand the operation of a device. To that end, open the ATmega328/P datasheet linked on our website from the Tools and Links Page. The TIMER1 we have used also has the ability to make an output pin turn ON (set), OFF (clear), or Toggle when the timer reaches the OCR1A value (i.e. in addition to generating an interrupt, the hardware can automatically toggle or turn an output on or off). By searching the data sheet and using the information in section 16.11.1 (especially table 16-1) answer the following questions:
    1. Which two pins can TIMER1 (the 16-bit) timer control (i.e. what pins are associated with OC1A and OC1B). Look at Figure 1.1 (upper right diagram of the "28 PDIP" package) on page 12 of the data sheet, or Table 14-3, 14-6, or 14-9 to find this info.
    2. 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;?