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
LP-MSPM0G3507 Launchpad Development Board or RPI-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 Interrupts
See also
The description provided below is mostly copied from Timer Interrupts.
Note
Reminder: All Timer HAL functions require the first argument, GPTIMER_Regs *timer, to be the timer being used: TIMGx or TIMAx.
There are 19 total interrupt sources that TIMGx or TIMAx may use to trigger an interrupt. These may be generated by the timer’s counting or by capture or compare events, among others. A subset of these are:
TIMER_INTSRC_ZERO: Occurs when the timer counter equals to 0.
TIMER_INTSRC_LOAD: Occurs when the timer counter equals to the configured period value.
TIMER_INTSRC_CCRn_DN: Triggered on a Capture or Compare event from CCRn when timer is counting DOWN. Replacenwith the desired CCR number.
TIMER_INTSRC_CCRn_UP: Triggered on a Capture or Compare event from CCRn when timer is counting UP. Replacenwith the desired CCR number.
TIMER_INTSRC_OVERFLOW: Triggered when the timer counter overflows from65535to0. The event should typically not occur in normal operation.
All of the above interrupt sources will trigger a single interrupt request handler per timer, or IRQ Handler; that is, each timer has a one IRQ Handler each. Where the IRQ Handler is simply just a function that runs when an interrupt occurs.
8.3.1.1. Enabling Interrupts
Each interrupt source may be individually enabled and disabled through the functions:
void Timers_enableInterrupt( GPTIMER_Regs *timer , uint32_t interruptMask )
void Timers_disableInterrupt( GPTIMER_Regs *timer , uint32_t interruptMask )
where uint32_t interruptMask is the desired interrupt source(s) to enable/disable. If multiple sources are desired, they are combined via a bitwise OR or enabled independently. These functions must be called after :code:`void Timers_initTimer()`, otherwise the requested change will be reverted.
Further, the associated timer interrupt must also be enabled in the CPU with
NVIC_EnableIRQ( IRQn_Type IRQn )
where IRQn_Type IRQn is a defined identifier corresponding to the TIMG or TIMA module to enable of the form: TIMGx_INT_IRQn or TIMAx_INT_IRQn, where x is the desired timer module number.
Each timer interrupt, if enabled, must have an associated IRQ Handler function. These functions must be named to pre-defined required names such that the interrupt requests are linked as desired. The naming convention for each IRQ Handler is:
void TIMGx_IRQHandler(void)orvoid TIMAx_IRQHandler(void)
where, again, x is replaced with the desired timer module number.
8.3.1.2. Interrupt Handling
The IRQ Handler associated with an interrupt will automatically be called when the specific interrupt is triggered. Within this function, the code must determine which interrupt source(s) triggered the handler. This may be done with one of three functions:
uint32_t Timers_getActiveInterrupt( GPTIMER_Regs *timer ):
Indicates the highest priority interrupt source pending for the corresponding module and will acknowledge the interrupt source. The function will return one of
TIMER_INTSRC_xdefined values as listed above. If using this identification method, the IRQ Handler will be called repeatedly until all enabled sources are acted upon.
uint32_t Timers_getPendingInterrupts( GPTIMER_Regs *timer ):
Returns all interrupt sources that are currently pending and enabled for the corresponding module without achnowledging any interrupt source. This is a bitwise OR combination of the corresponding
TIMER_INTSRC_xdefined values as listed above. If using this identification method, only one IRQ Handler call is needed for all sources to be acted upon.
uint32_t Timers_getAllPendingInterrupts( GPTIMER_Regs *timer ):
Returns all interrupt sources that are currently pending, including disabled sources, for the corresponding module without achnowledging any interrupt source. This is a bitwise OR combination of the corresponding
TIMER_INTSRC_xdefined values as listed above. If using this identification method, only one IRQ Handler call is needed for all sources to be acted upon. This function should only be used in Activity 7.
If using the second or third functions as given above, each enabled interrupt source must be acknowledged (done automatically using the first function). If this acknowledgment is not done, the interrupt will continuously trigger; that is, the IRQ Handler will be immediately called again once it completes, erroneously. To acknowledge, or clear an interrupt source, use the following function:
void Timers_clearInterrupt( GPTIMER_Regs *timer , uint32_t interruptMask )
where uint32_t interruptMask is the desired interrupt source(s) to acknowledge and clear. If multiple sources are desired to be acknowledged, they are combined via a bitwise OR or enabled independently.
Two example IRQ Handlers are provided here.
8.3.1.3. Steps for Timer Interrupt Setup
Summarizing the above, a timer interrupt can be enabled by the following steps:
Configure the timer as normal,
Enable the desired interrupt source(s) via
void Timers_enableInterrupt(),Enable the CPU interrupt via
void NVIC_EnableIRQ(),Write the corresponding IRQ Handler,
Within the IRQ Handler:
Identify the active interrupt source or pending interrupt source(s), (see examples)
Acknowledge pending interrupt sources as needed via
void Timers_clearInterrupt()
8.3.2. GPIO Interrupts
See also
This is a condensed discussion of GPIO interrupts. A more thorough discussion may be found here
A GPIO pin can trigger an interrupt when its value changes: either transitions from low-to-high or high-to-low. These events are known as a rising edge and falling edge, respectively. A pin can be configured to trigger an interrupt on either or both edges.
GPIO Signal Edges
All pin interrupt triggers will invoke a single IRQ Handler, for the microcontroller (at least for the MSPM0G3507). This implies that if multiple pins are configured to generate interrupts, there must be code to support determining what pin triggered the interrupt, regardless of port.
8.3.2.1. Enabling Interrupts
Each GPIO pin may have its interrupt capability enabled and disabled using the functions:
void GPIO_enableInterrupt( GPIO_Regs* gpio, uint32_t pins)
void GPIO_disableInterrupt( GPIO_Regs* gpio, uint32_t pins)
Further, selection of the rising or falling edge as the interrupt event may be done through the function:
void GPIO_setInterruptPolarity( GPIO_Regs* gpio , uint32_t pins , gpio_int_polarity_t polarity )
where gpio_int_polarity_t edgeSelect is either GPIO_INT_EDGE_RISE, GPIO_INT_EDGE_FALL, or GPIO_INT_EDGE_RISE_FALL.
The code may check if a pin(s) has interrupt capability enabled by using:
uint32_t GPIO_getEnabledInterrupts( GPIO_Regs* gpio , uint32_t pins )
Finally, the single interrupt for all GPIO pins must be enabled within the CPU with
NVIC_EnableIRQ( IRQn_Type IRQn )
where IRQn_Type IRQn is set as GPIO_INT_IRQn.
8.3.2.2. Interrupt Handling
The ISR or IRQ Handler associated with the GPIO interrupts will be automatically called when the selected edge is detected on any of the enabled pins. Within this function, the code must acknowledge the interrupt by clearing the associated flag, or register bit that indicates the interrupt was triggered. If the flag is not cleared, the interrupt will continuously trigger; that is, the function will be immediately called again once it completes.
The IRQ Handler for the purposes of this class must be named void GPIO_IRQHandler().
Within void GPIO_IRQHandler(), The interrupt source must be acknowledged using:
void GPIO_clearInterrupt( GPIO_Regs* gpio , uint32_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:
uint32_t GPIO_getPendingInterrupts( GPIO_Regs* gpio )
where this function returns a value that is a bitwise-or, |, of all pins within the specified port that have triggered the interrupt. For example, if pushbuttons on PA1 and PA3 were pressed simultaneously, this function would return the value GPIO_PIN1 | GPIO_PIN3. This function will only return triggers that are currently enabled. Alternatively, the function:
uint32_t GPIO_getAllPendingInterrupts( GPIO_Regs* gpio )
will return the interrupt trigger status for all pins in the port, regardless of if they are enabled or not.
Important
Both uint32_t GPIO_getPendingInterrupts() and uint32_t GPIO_getAllPendingInterrupts() only return pending interrupts for the requested port. However, the void GPIO_IRQHandler() will be triggered by pins on both ports; therefore, these functions will need to be called twice (once for each port) if both ports have enabled pin interrupts.
An example process for handling two enabled interrupt pins, assuming that both PA10 and PA11 are connected to pushbuttons that return HIGH when pressed.
// This example assumes that PA10 and PA11 are configured for
// triggering an interrupt on a GPIO_INT_EDGE_RISE edge
// This code would be within the GPIO_IRQHandler() function
// First, we'll debouce the press to avoid multiple triggers for one press:
delay_cycles(320e3); // 10 ms delay (32 MHz clock)
// Next: get the list of pins that may have triggered the interrupt
uint32_t active_pins = GPIO_getPendingInterrupts(GPIOA);
// Check to see if PA10 interrupt is active
if(active_pins & GPIO_PIN10){
// Clear the GPIO_PIN10 interrupt
GPIO_clearInterrupt(GPIOA,GPIO_PIN10);
// It is possible that bouncing from the a falling-edge transition can also
// trigger the interrupt. The if statement below is ensuring that the pin's
// value after debouncing (above) is HIGH; which verifies that the transition
// was in fact a rising-edge, not falling-edge
if(GPIO_readPins(GPIOA,GPIO_PIN10)){
// PA10 press detected, do whatever is needed
}
}
// Repeat for GPIO_PIN11 (PA11).
if(active_pins & GPIO_PIN11){
GPIO_clearInterrupt(GPIOA,GPIO_PIN11);
if(GPIO_readPins(GPIOA,GPIO_PIN11)){
// PA11 Press detected, do whatever is needed
}
}
// Repeat as necessary for more pins
8.3.2.3. Steps for GPIO Interrupt Setup
Summarizing the above, a GPIO interrupt can be enabled by the following steps (very similar to Timer requirements):
Configure GPIO pin as normal,
Enable the pin to generate an interrupt trigger via
void GPIO_enableInterrupt(),Select the desired trigger edge via
void GPIO_setInterruptPolarity(),Enable the CPU interrupt via
void NVIC_EnableIRQ(),Write the corresponding IRQ Handler,
Within the IRQ Handler:
Identify the pending interrupts by pins via
uint32_t GPIO_getInterrupts(), for both portsAcknowledge pending interrupts by pin via
void GPIO_clearInterrupt()
8.4. Instructions
This activity uses a custom template:
activity_stopwatch.zip. Locations 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.Within
GPIOInit(), initialize PA0 (LED1) as an output.Copy the timer initialization code from Activity 7 and modify it to:
Enable the Zero or Load event interrupt source,
Enable the TIMG7 interrupt, and
Prevent the timer from starting in this initialization function.
Create the
void TIMG7_IRQHandler()interrupt function. Within this function, add code to:Acknowledge the selected interrupt source,
Toggle LED1 every second (you can copy the necessary code from Activity 7),
Update both the lap and total time arrays (
lap_timeandtotal_time) every iteration by callingvoid UpdateTime(uint8_t * time)appropriately twice.Mark that the time should be printed by setting the already defined variable
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).
For initial testing, trigger the timer to start counting 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 timer.Within
GPIOInit(), 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),
the pins are enabled interrupt sources, and
the GPIO interrupt is enabled.
Stopwatch control pushbuttons, enable with pull-up resistors control
Launchpad
RPI-RSLK
Start/Stop
PB21 (S2)
PA7 (BMP1)
Lap/Reset
PB1
PB25 (BMP6)
Note
Switch S1 on the Launchpad Board is unusable as it has a secondary function with the board that will cause issues if changed. Instead, for the second switch either wire a pushbutton to ground on PB1 OR simply use a wire to connect/disconnect PB1 to ground, acting like a switch.
As each configured button will trigger the GPIO IRQ Handler when pressed, it is necessary to determine which button was pressed. Use the example code above to provide this structure.
Implement the desired 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 HAL 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 1 (applicable variable already exists),
Indicate that the reset time values should be printed using the
print_flagflag.
If the timer is running, 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
0in theif(0)to check if a new lap has started.Within this
ifstatement: clear the flag, reset the lap time array, and increment the lap number.
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\n") could be used in the interrupt. These debugging prints should be immediately removed once the debugging of the specific functionality is complete. Alternatively, GPIO pins may be toggled in the interrupt (as done with PA0) to ensure functionality.
Footnotes