Timer Interrupts

Configuring timer interrupts tends to be microcontroller-specific (see, for example, the section on the ATmega328P’s Timers and the RP2040’s Timers). The availability of convenience functions can be hit-or-miss. For example, avr-libc offers no convenience functions to configure timers and enable timer interrupts, and it and offers only its primitive ISR() macro for registering timer ISRs. On the other hand, Mbed OS provides convenience functions to manage timer interrupts and their ISRs, for both one-off timer events and for periodic timer events.

The Cow Pi library offers convenience functions to configure timers, enable interrupts, and register timer ISRs for periodic timer events. The particulars are, nonetheless, dependent on the target’s architecture. There currently is no support for any microcontrollers’ underlying waveform generation modes; however, waveforms can easily be created with periodic timer interrupts.

Important

As with all ISRs, you want to keep your interrupt handler short. An ISR that requires a significant fraction of the timer’s period (or longer) to execute will cause undesirable behavior!

Sharing data with ISRs

Regardless of whether you configure interrupts using the register_periodic_ISR function or a platform-specific mechanism, data cannot be passed to the interrupt-handling code through parameters, and the interrupt-handling code cannot return data through a return value. This necessitates the use of global variables to provide data to, and obtain data from, the interrupt-handling code.

Because the compiler cannot detect any definition-use pairs for these global variables – they are updated in one function and read in another, and no call chain exists between the two functions – the compiler will optimize-away these variables and the code that accesses them in the interest of reducing the program’s memory footprint. The way to prevent this mis-optimization is to use the volatile keyword.

Important

Any global variables that interrupt-handling code reads from and/or writes to must have the volatile modifier.


Differences in target architecture require differences in how the timer interrupts are set up.

Configuring AVR Timers and Registering ISRs for Interrupts

AVR targets have multiple timers, each of which can support two or three periodic interrupts, depending on exactly how it’s configured. The configure_timer function provides a convenient way to configure any timer except TIMER0. The timer period is not limited to a whole number of microseconds, and so the timer period (measured in microseconds) can be passed as a float value. Unfortunately, the timers cannot take on arbitrary timer periods; indeed, there are many intervals that are a whole number of microseconds that cannot be accurately assigned. For this reason, configure_timer returns the timer’s actual period, which will be the closet-possible timer period.

See the microcontroller-specific details (ATmega328P: Timers) if you wish to determine the possible exact timer periods and which timer periods would permit three periodic interrupts for the timer.

float configure_timer(unsigned int timer_number, float desired_period_us)

Configures an AVR timer.

The timer will be configured to reset its counter at or near the specified interval. The function will select the configuration that has an actual period as close as possible to the specified period.

The timer might be configured for Normal mode or for CTC mode; if a Normal mode configuration and a CTC mode configuration are equally-accurate then the timer will be configured in Normal mode. WGM mode is not supported.

Any ISRs that had previously been registered for the timer will be deregistered.

This function may not be used to configure TIMER0.

Parameters:
  • timer_number – The timer to be configured

  • desired_period_us – The preferred interrupt period

Returns:

The actual interrupt period

After the timer is configured, ISRs can be registered with the register_periodic_timer function by specifying the timer, the ISR slot, and the ISR. The function will return false if ISR slot 2 is specified but is unavailable, or if the timer has not been configured. The return value must be checked.

bool register_periodic_ISR(unsigned int timer_number, unsigned int isr_slot, void (*isr)(void)) __attribute__((warn_unused_result))

Registers a function to service periodic timer interrupts.

Each timer has ISR slots for two or three interrupt service routines: two if the timer is in CTC mode, three if the timer is in Normal mode. Regardless of the timer mode, the ISR registered for slot 0 will execute when the counter resets. If the timer is in CTC mode, then the ISR registered for slot 1 will execute halfway between counter resets. If the timer is in Normal mode, then the ISRs registered for slots 1 and 2 will partition the timer period into thirds.

The timer must have previously been registered using configure_timer().

This function may not be used to register ISRs for TIMER0.

Parameters:
  • timer_number – The timer whose interrupt will invoke the ISR

  • isr_slot – 0 if the ISR should be invoked when the counter resets, 1 or 2 otherwise

  • isr – The function that will service the timer’s interrupts

Returns:

true if the ISR was successfully registered; false otherwise

Configuring ARM Timers and Registering ISRs for Interrupts

ARM targets have a single timer that can support multiple interrupts. Unlike AVR targets, only integer values can be used to specify the interrupt period (in microseconds); however, every representable positive 32-bit integer (1 ≤ T < 4,294,967,296) can be used exactly as the timer period. Because the hardware timer itself does not need to be configured, configuring the virtual timer and registering the ISR is accomplished with a single function.

MAXIMUM_NUMBER_OF_TIMERS

8

bool register_periodic_ISR(unsigned int timer_number, uint32_t period_us, void (*isr)(void)) __attribute__((warn_unused_result))

Configures a periodic timer interrupt to fire, and assigns a function to service that interrupt.

This function supports up to MAXIMUM_NUMBER_OF_TIMERS timers.

Any ISR that had previously been registered for the timer will be deregistered.

Parameters:
  • timer_number – A unique handle for the virtual periodic timer being configured

  • period_us – The specified interrupt period

  • isr – The function that will service the timer’s interrupts

Returns:

true if the periodic interrupt was successfully configured and the ISR was successfully registered; false otherwise

Resetting the Timer Interrupt Period

From time to time, you may have an application that requires you to “reset” the timer period; that is, postpone the next interrupt until a full timer period after now. On AVR targets, this can be accomplished simply by writing a 0 to the timer’s counter register. On ARM targets, writing a 0 to the timer’s counter register is inadvisable since the (only) timer’s counter is generally assumed to increment monotonically.

The CowPi library provides a platform-independent mechanism to accomplish this reset that will produce the desired effect in a manner consistent with the platform’s requirements.

void reset_timer(unsigned int timer_number)

Sets a timer to the beginning of its interrupt period.

If the interrupt period is T microseconds, then:

  • On AVR architectures, the ISR in slot 0 will execute T microseconds after reset_timer() returns. ISRs in other slots will fire sooner, according to their place in the timer period’s partitioning. The underlying timer’s counter will reset to 0.

  • On MBED systems, the ISR will execute T microseconds after reset_timer() returns. The underlying timer’s counter will be unchanged.

Note

On AVR architectures, this affects all ISR slots for the specified timer.

Parameters:

timer_number – The timer to be reset

Counting TIMER0 Overflows on AVR Targets

You might wish to create a function that returns the elapsed time since the system was booted up. On ARM targets, this can be implemented simply by reading the timer’s counter value. On AVR targets, the timers’ counters overflow too frequently for this to be useful.

Instead, the CowPi library offers a way to obtain the number of times that TIMER0 has overflowed. Based on the way that the Arduino framework configures TIMER0, we know that each increment of the counter corresponds to 4µs, and each overflow corresponds to 1024µs.

void initialize_timer0_overflow_count(void)

Enable TIMER0 comparison interrupt to support get_timer0_overflow_count()

unsigned long get_timer0_overflow_count(void)

Returns the number of times that TIMER0 has overflowed.

The library counts the number of times that TIMER0 has overflowed (the Arduino core doesn’t expose its own count, so CowPi has to do its own). When combined with the current value of TIMER0’s counter, a student can compute the number of microseconds since power-up using the formula: 1024 * timer_overflow_count + 4 * timer0_counter_value

There are two race conditions that could result in inconsistent values among the timer overflow count and the timer counter value, but we accept the risk. The race condition can be minimized by calling get_timer0_overflow_count() first and then reading the counter value is read, then these windows partially cancel each other out, then the effective race condition window will be 1.375us out of every 1024us.

Returns:

the number of times that TIMER0 has overflowed