8. Timers: Stopwatch

8.1. Purpose

This activity demonstrates the use of interrupts and event-based programming in creating a “stopwatch.”

8.2. Hardware and Tools

  • MSP-EXP432P401R Launchpad Development Board or TI-RSLK Robotic Car

8.2.1. Project Template

8.3. Description

The method to detect the Timer_A reset event used in the previous activity is inefficient and may lead to significant error due to evaluation speed. A more efficient alternative is achieved when using interrupts to automatically detect a timer reset.

The “Stopwatch” functionality of this activity keeps track of time to one-hundredth of a second. Control of the program is provided by two bumpers: one bumper press serves as “Start/Stop”, which will of course start and stop the timer; and the other serves as “Lap/Reset”, where if this bumper is pressed when the timer is running, a new lap is started; whereas if it is pressed when the timer is stopped, the timer and laps are reset. Both the timer and bumper functionality will be implemented using interrupts.

8.3.1. Timer_A Interrupts

See also

This is a condensed discussion of Timer_A interrupts. A more thorough discussion may be found here.

Note

Reminder: All Timer_A DriverLib functions require the first argument, uint32_t timer, to be the timer being used: TIMER_Ax_BASE.

There are several ways that Timer_A can trigger an interrupt:

  • A timer counter reset event (also referred to as the timer overflow), or

  • A capture/compare event from one of the CCRs.

In this activity, we will only focus on the timer reset interrupt trigger. This trigger, known simply as the Timer_A interrupt, or TAI, may be enabled by setting the corresponding field in the configuration struct: .timerInterruptEnable_TAIE.

Along with enabling the interrupt, the program must associate a segment of code to run when the interrupt is triggered. This is called “registering” a function as an interrupt service routine (ISR) and may be done through the function:

void Timer_A_registerInterrupt( uint32_t timer , uint8_t interruptSelect , <function_name> )

where the argument uint8_t interruptSelect is either:

  • TIMER_A_CCR0_INTERRUPT: associate the interrupt function with the Timer_A_CCR0_captureCompare interrupt

  • TIMER_A_CCRX_AND_OVERFLOW_INTERRUPT: associate the interrupt function with the Timer_A interrupt or a CCR interrupt generated by CCR1-CCR4.

The final argument, <function_name>, is simply the name of the function to serve as the ISR without parenthesis. This function can be named anything, but shouldn’t take any arguments or produce any return value; that is, it should be of the format void function_name(void).

Within the ISR function, the interrupt source must be acknowledged such that the ISR is not called repeatedly. This may be done through the function:

void Timer_A_clearInterruptFlag( uint32_t timer )

The function then must address what needs to be done each time the interrupt is detected. For many applications, this is simply setting a global variable to 1 to indicate the event happened.

An example ISR implementation for the timer may be as simple as:

// Other includes/prototypes, etc.
void Timer_ISR(); // ISR function prototype

uint16_t timer_reset_count; // variable to count timer interrupts
                            // this variable must be globally defined

void main(){
    ...
}

void Timer_Init(){
    ...
    // Registering the ISR...
    Timer_A_registerInterrupt(TIMER_A2_BASE,TIMER_A_CCRX_AND_OVERFLOW_INTERRUPT,Timer_ISR);
    ...
}

// Timer ISR function
void Timer_ISR(){ // Name doesn't matter
    Timer_A_clearInterruptFlag(TIMER_A2_BASE);   // acknowledge the interrupt
    timer_reset_count++;                         // increment our counter
}

where the global timer_reset_count may be used within the main() function (or others) to measure time. Note that within the example Timer_ISR() there is no check for what triggered the interrupt as only the reset/overflow interrupt is enabled (not shown).

8.3.2. GPIO Interrupts

See also

This is a condensed discussion of GPIO interrupts. A more thorough discussion may be found here

Note

Reminder: GPIO DriverLib functions require the first argument, uint8_t port, to be the target port: GPIO_PORT_Px. Some functions requiring selection of pins, uint8_t pins, using GPIO_PINy.

The MSP432 is capable of generating interrupt triggers when a GPIO pin changes value. These events are grouped by ports when triggering the interrupt service routine; therefore, there may be only one ISR per port. This implies that if multiple pins within one port are configured to generate interrupts, there must be code to support determining what pin triggered the interrupt.

Initializations of GPIO interrupts follows a similar path as the timer setup:

  1. Configure the pin as normal,

  2. Register the ISR function for the port. This may be done through the function:

    void GPIO_registerInterrupt( uint8_t port , <function_name> )

  3. Enable the interrupt. This step requires two parts:

  1. The selection of the value change, or edge, on which to trigger via:

    void GPIO_interruptEdgeSelect( uint8_t port , uint8_t pins , uint8_t edgeSelect )

    where edgeSelect is either GPIO_LOW_TO_HIGH_TRANSITION or GPIO_HIGH_TO_LOW_TRANSITION.

  2. Then the interrupt trigger may be enabled for the desired pins with:

    void GPIO_enableInterrupt( uint8_t port , uint8_t pins )

Within the ISR function, the interrupt source must be acknowledged using:

void GPIO_clearInterruptFlag( uint8_t port , uint8_t pins )

If multiple pins are enabled as trigger sources, it is necessary to determine which pin triggered the interrupt. This may be done through the function:

uint16_t GPIO_getEnabledInterruptStatus( uint8_t port )

where this function returns a value that is a bitwise-or, |, of all pins within the specified port that (could have) triggered the interrupt. For example, if pushbuttons on P3.1 and P3.3 were pressed at the exact same time, this function would return the value GPIO_PIN1 | GPIO_PIN3; therefore, this output value must be masked to determine each active pin. This function must be called before clearing the interrupt flag(s). An example code segment within the ISR to determine and handle presses is given below.

// This example assumes that P3.1 and P3.3 are configured for
// triggering the interrupt on a GPIO_HIGH_TO_LOW_TRANSISTION
// This code would be within the function registered as the port 3 ISR

    // First, we'll debouce the press to avoid multiple triggers for one press:
__delay_cycles(240e3); // 10 ms delay (24 MHz clock)

    // Next: get the list of pins that may have triggered the interrupt
uint8_t active_pins = GPIO_getEnabledInterruptStatus(GPIO_PORT_P3);

    // Check to see if GPIO_PIN1 interrupt is active
if(active_pins & GPIO_PIN1){
        // Clear the GPIO_PIN1 interrupt
    GPIO_clearInterruptFlag(GPIO_PORT_P3,GPIO_PIN1);
        // It is possible that bouncing from the LOW_TO_HIGH transition can also
        // trigger the interrupt. The if statement below is ensuring that the pin's
        // value after debouncing (above) is LOW; which verifies that the transition
        // was in fact a HIGH_TO_LOW transition, not LOW_TO_HIGH
    if(!GPIO_getInputPinValue(GPIO_PORT_P3,GPIO_PIN1)){
        // P3.1 press detected, do whatever is needed
    }
}
    // Repeat for GPIO_PIN3 (P3.3).
if(active_pins & GPIO_PIN3){
    GPIO_clearInterruptFlag(GPIO_PORT_P3,GPIO_PIN3);
    if(!GPIO_getInputPinValue(GPIO_PORT_P3,GPIO_PIN3)){
        // P3.3 Press detected, do whatever is needed
    }
}
// Repeat as necessary for more pins

8.4. Instructions

  1. This activity uses a custom template: activity_stopwatch.zip. Location in the code where edits are required are marked with **ACTIVITY**. Some portions of the final Activity 7 code will be copied for use in this activity.

  2. This program will use two interrupt functions: one for the timer reset and one for pushbutton presses. Make two ISR functions: void Port4_ISR() (or void Port1_ISR() if using Launchpad Board) and void Timer_ISR(). We will write the code for these functions in a later step.

  3. Within GPIO_Init(), initialize P1.0 (LED1) as an output.

  4. Copy the timer initialization code from Activity 7 and modify it to:

    • Enable the Timer_A interrupt (overflow/reset interrupt) by adding the necessary field to the configuration struct, and

    • Register the function Timer_ISR() with this interrupt.

    • Prevent the timer from starting in this initialization function.

  5. Within Timer_ISR(), add code to:

    • Acknowledge the timer overflow interrupt,

    • Toggle LED1 every second ( you can copy the necessary code from Activity 7)

    • Update both the lap and total time arrays (lap_time and total_time) by calling void UpdateTime(uint8_t * time) appropriately.

    • Mark that the time has been updated and it should be printed by setting the already defined flag print_flag. This is an instance of a flag, which are variables/bits used to indicate an event has happened and are typically either true or false. “Setting” a flag is as simple as setting the value to 1 (true).

  6. For initial testing, trigger the timer to start counting in UP mode after the initialization functions are called in the main() function. Running the code should result in LED1 blinking appropriately and the stopwatch time printed on the terminal. Once done verifying this functionality, comment out the line of code used to start the counter.

  7. Within GPIO_Init(), initialize two switch inputs (see table below, pull-up resistors required) such that:

    • they generate interrupts on transition from HIGH to LOW (button is being pressed), and

    • the interrupts will trigger Port4_ISR() function. (or Port1_ISR()).

    Stopwatch control pushbuttons, enable with pull-up resistors

    control

    Launchpad

    TI-RSLK

    Start/Stop

    P1.1 (S1)

    P4.0 (BMP0)

    Lap/Reset

    P1.4 (S2)

    P4.2 (BMP1)

  8. As each configured button will trigger Port4_ISR() (or Port1_ISR()) when pressed, it is necessary to determine which button was pressed. Use the example code above to implement this functionality.

  9. Implement the button functionality within the applicable place in the ISR. For the button that controls:

    • Start/Stop: Start the timer if it is stopped and stop it if it is started. As the DriverLib does not provide a way to check the run status of the timer, you will need to keep track of it with another flag; define one.

    • Lap/Reset: [1]

      • If the timer is stopped (determined from the flag above), do the following within the interrupt:

        • Clear both the lap and total time arrays (set elements to 0),

        • Reset lap number to 0 (applicable variable already exists),

        • Indicate that the reset time values should be printed using the print_flag flag.

      • If the timer is not stopped, do the following:

        • In the interrupt: Set flag to denote that a new lap has started. This flag must be created

        • In the main loop: replace the 0 in the if(0) to check if a new lap has started.

        • Within this if statement: clear the flag, reset the lap time array, and increment the lap number.

  10. Test the program. If all is good, submit the final code to the corresponding assignment in Gradescope along with a screenshot of the stopwatch working with several laps triggered.

Hint

Debugging Interrupts

In the lecture, it was stated that printf() should not be used in the interrupt as it is slow and can interfere with printf() calls outside of interrupts. However, it is perfectly acceptable to temporarily place printf() statements in interrupts when debugging functionality. For example: to verify an interrupt is called, a short printf("here\r\n") could be used in the interrupt. These should be removed once the debugging of the specific functionality is complete. Alternatively, GPIO pins may be toggled in the interrupt (as done with P1.0) to ensure functionality.

Footnotes