EmbeddedRelated.com
Blogs
The 2024 Embedded Online Conference

Introduction to Microcontrollers - Timers

Mike SilvaSeptember 27, 20132 comments

Quick Links

Timers - Because "When" Matters

Computer programs are odd things, for one reason because they have no concept of time.  They may have the concept of sequential execution, but the time between instructions can be essentially any number and the program won't notice or care (unless assumptions about time have been built into the program by the programmer).  But the real world is not like this.  In the real world, especially the real embedded world, time is always a factor.  A program has to fire a sparkplug so many microseconds before top dead center.  The oil pressure has to be in the safe range so many seconds after the engine starts.  The digital clock has to know exactly how long consitutes one second.  The sleep mode routine has to know how long since the last user input.  The list could go on indefinitely.

Because time is so important in the embedded programming world, all microcontrollers have one or more built-in hardware timers, designed to run independently of the CPU.  All timers are fundamentally pulse counters, and sometimes you will see a timer referred to as a "timer/counter."  If the incoming pulses are of a fixed frequency, counting them becomes a timing function.  Timers can also be configured to count incoming pulses that are not of a fixed frequency, but are essentially random, such as e.g. the ticks of a Geiger counter.  In that case the timer (which IS a counter anyway) is truly acting as a counter.  We will talk about using timers as counters in a future chapter.  For this chapter, we will just discuss timers as timers.

This article is available in PDF format for easy printing

We will be considering just timers that count up in this chapter, since that is what both the AVR and the STM32 timers do, but some timers can be configured to count up or down (actually, some STM32 timers can count up or down, but we will just use all STM32 counters in up-mode for now).  Just keep in mind that what follows assumes timers that are counting up.

How Many Bits of Time?

Timers are specified by the number of bits their counters are comprised of.  Timers are typically 8, 16 or 32 bits, making it easy to read and write them using standard 8, 16 or 32 bit variables.  It is not unusual to find timers of different sizes on the same microcontroller.  For example, most AVRs have both 8-bit and 16-bit timers, and some NXP Cortex M3 parts have both 16-bit and 32-bit timers.  As a reminder, an 8-bit timer can count 2^8 or 256 input clocks, a 16-bit timer 2^16 or 65536 input clocks and a 32-bit timer 2^32 or over 4 billion input clocks.  After that the timers simply roll over or "overflow" back to 0 and continue counting.

Larger timers (more bits) are more versatile, but they are also more expensive in terms of chip area.  With a 1 MHz input clock, an 8-bit timer can only time (without some trickery) a maximum of 256 microseconds, while a 16-bit timer can time a maximum of 65,536 microseconds, and a 32-bit timer - well, lots!

For our AVR examples in this chapter we will use timer 1, which is a 16-bit timer.  For the STM32 examples we will use timer 1, which is a 16-bit timer with a 16-bit prescaler.

Clock In, Ticks Out

The steady stream of pulses that a timer counts is usually called the timer "clock."  The steady time output signals that a timer can be configured to produce are usually called "ticks."  A timer clock can be the same as the CPU (or system) clock, or it can be a lower frequency, or in some cases it can even be a higher frequency.  For now we will only talk about the case where the timer clock is equal to or lower in frequency than the system clock.

Clock management in more complex microcontrollers can get complicated, but to start with, we'll just imagine that the system clock signal (the clock driving the CPU) is what drives the timers.  This clock signal will go into a divider or counter called the "prescaler."  The output of the prescaler is the clock input to the timer.  By changing the number by which the prescaler divides the incoming clock, we can change the frequency of the timer clock even though the system clock frequency remains the same.  The ability to change the timer clock frequency lets us choose the most suitable frequency for the timing job(s) at hand.  A faster timer clock gives higher time resolution but a shorter maximum time, while a slower timer clock gives lower time resolution but a longer maximum time.

Some prescalers are limited to just power-of-2 divisions of the system clock, so you can get F (the system clock) or F/2 or F/4 or F/8 or F/16 or ... to some maximum division 2^N.  Some prescalers are even more limited than this, in that only certain power-of-2 divisions are available, not all of them.  The AVR prescalers are like this.  For most AVRs, the only prescaler divisions you get are 1, 8, 64, 256 and 1024.

Other prescalers are full-blown configurable counters that can divide the system clock by any integer value.  The STM32 prescalers are like this.  They are 16-bit prescalers so the system clock can be divided by any value between 1 and 65536.  Such prescalers are much nicer to work with due to their greater flexibility, but are not a must-have.

Simple Timer Tricks

Here are the main things we can do with a simple timer:

  • Select prescaler value
  • Write timer register
  • Read timer register
  • Start and Stop timer
  • Detect if timer overflows (goes from MAX count 2^N-1 to 0)

These should all be pretty self-explanatory.  It should be pointed out that when a timer overflows it sets a flag, and this flag can be inspected directly in code, or it can be used to generate an interrupt.  Thus even a simple timer can generate a steady interrupt "tick," useful for monitoring the passage of time in the user program.

A timer tick ISR is a fundamental building block for many, perhaps most, embedded systems.  A timer tick on the order of 10ms is useful for a variety of tasks, since it provides good responsiveness for human actions while still allowing most of the CPU cycles to go towards other work.  A tick in this range could be achieved on a 1MHz AVR, for example, by using an 8-bit timer and a prescale value of 64, giving a timer overflow period of 256*64 or 16,384 microseconds - about 16 milliseconds, plenty close enough to the 10ms "standard" to do the job.  This is about the simplest timer interrupt you can get, and especially with a more configurable prescaler, it may be all you need for many applications.

Another useful thing one can do with such a simple timer is to use the overflow interrupt to write a new starting value into the timer (the overflow itself will have set the timer to 0).  For instance, taking our example above, if on every overflow we write a value of 100 into the timer, now instead of having 256 timer clocks between each overflow, we have 156 timer clocks - the timer now ticks from 100 to 255, not from 0 to 255.  With a 1MHz system clock and a prescale of 64, we now have a timer overflow period of 156*64us or 9984us - much closer to our target of 10ms, if we really determined we needed to get as close as possible to that target.  Or, perhaps using a more real-world consideration, we may want to generate a tick frequency that is an exact integer.  This will let us count ticks to get exactly 1 second time intervals, very useful for creating and displaying human-friendly time values.  Doing a bit of math with our 1MHz clock and 64 prescaler, we have 1,000,000/64 = 15625, which is 125*125.  If we set our timer to count 125 prescaled clocks that will give us an 8ms tick (125Hz tick rate, an exact integer), and if we count 125 of these ticks in software we get exactly 1 second.  Thus we would reload our timer with 256-125=131 to get our 8ms tick.  The reason we need a tick frequency that is an exact integer is that we can only count by exact integers - we can't count e.g. 125.75 ticks!

There's Always A Catch

This reloading technique is not foolproof.  Remember that an ISR does not run immediately when an interrupt request is generated.  Further, if other interrupts are enabled, or if global interrupts are disabled at points during the program execution, the timer interrupt response may be further delayed.  Then there is the hidden state-saving code at the beginning of the ISR which executes before any user ISR code executes.  All of this adds up to the reality that it takes a while for the user ISR code to execute after a timer interupt, and that time may vary from interrupt to interrupt.  All the while, the timer is running.  The timer has rolled over to 0 when the overflow interrupt request is generated, but is it still at 0 when the user ISR code finally reloads the timer?  If the prescale value is low (the timer is ticking up every CPU instruction, or every few instructions), the timer may have ticked ahead to 1 or 2 or 5 or who knows.  Imagine that we are trying to generate a timer interval of 100 timer clocks, by loading 156 into our timer in the ISR.  Also imagine that our timer has counted up to 5 before our ISR code runs to reload the timer.  This means that instead of our next interval being 100 timer clocks, it will be 105 timer clocks - the 100 we get by reloading the timer, and the 5 that ticked off before we got around to reloading the timer.  105 may be close to 100, but often close is not enough.

It turns out that we can overcome most, but not all, of this problem by subtracting the current timer value from our interval value.  This actually involves adding the current timer value to our reload value, since a higher reload value results in a shorter interval for an up-counting timer.  Using our example numbers from above, if the timer was at 5 when we reloaded it, we would reload with a value of 156+5=161, giving us 95 more timer clocks to overflow in addition to the 5 that have already occurred.  This will reduce the cumulative timing error using the timer reload technique, but it will not eliminate it, because the timer can always tick forward while we're doing this addition and assignment.  Lucky for us, there is a better way.  But first, let's blink our LED with a timer and see how it gives us back all those CPU cycles we wasted in using software delays.

Blinking An LED With A Timer

This program will actually blink two LEDs.  The first will be our old friend the software delay blinking LED.  The second will be a timer-driven blinking LED.  You will see how the first approach uses literally millions of clock cycles for one LED blink (most wasted in the delay function), while the timer-driven approach uses a few dozen cycles to achieve the same result (and with more precision!).

LED Blink Using Overflow - AVR Version

Here we set up our 16-bit timer to generate a 250ms overflow with interrupt.  Since we are sharing the PORTB resource, we have to protect PORTB when we access it in the background loop by disabling and then re-enabling interrupts.  Also note that we are getting away from magic numbers like 31250 and using the relevant equation directly to determine the timer reload value.  Since the number is a constant it will be calculated at compile time and does not have any runtime cost.

// AVR_TIM1
// toggle PB0 LED with software delay
// toggle PB1 LED in overflow ISR
// AVR clock is 8MHz

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU      8000000UL  // 1MHz
#define PRESCALE   64
#define PERIOD     250        // milliseconds - 1/4 second delay
#define T1_CLKS    ((F_CPU/PRESCALE*PERIOD)/1000) // 1000ms per s

ISR(TIMER1_OVF_vect)
{
  TCNT1 += (65536UL-T1_CLKS);  // count up 31250 to overflow
  PORTB ^= (1<<PB1);           // toggle PB1
}

void delay(volatile uint32_t d)
{
  while (d-- != 0)
    ;
}

int main(void)
{
  DDRB = 3;                    // PB0, PB1 outputs
  
  TIMSK = (1<<TOIE1);          // enable overflow interrupt
  TCCR1A = 0;                  // normal mode
  TCCR1B = 3<<CS10;            // /64 prescaler
  TCNT1 += (65536UL-T1_CLKS);  // initial load
  
  sei();
  
  while(1)
  {
    cli();
    PORTB ^= (1<<PB0);         // toggle PB0 
    sei();
    
    delay(80000);
  }
}

LED Blink Using Overflow - STM32 Version

Very similar to the AVR version.  All the STM32 timer interrupt flags must be reset in their corresponding ISRs by writing a 0 to the flag.

// STM32_TIM1
// toggle LED on PC9 using software delay
// toggle LED on PC8 using timer 1 with ISR reload
// Default 8 MHz internal clock

#include <stm32f10x.h>

#define F_CPU			8000000UL	// 1MHz
#define PRESCALE		64
#define PERIOD			250	// microseconds - 1/4 second delay
#define TCLKS			((F_CPU/PRESCALE*PERIOD)/1000)

void TIM1_UP_TIM16_IRQHandler(void)
{
  TIM1->CNT += (65536UL-TCLKS); // reload
  GPIOC->ODR ^= (1<<8);         // toggle blue LED
  TIM1->SR = ~TIM_SR_UIF;       // clear update int flag (write 0)
}

void delay(volatile uint32_t d)
{
  while (d-- != 0)
    ;
}

int main(void)
{
  RCC->CFGR = 0;                      // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // enable PORTA

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // enable PORTC
  GPIOC->CRH = 0b0010 | (0b0010 << 4);// CNF=0, MODE=2 (2MHz output) PC8,PC9

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // enable Timer1

  TIM1->PSC = PRESCALE - 1;
  TIM1->DIER = TIM_DIER_UIE;          // enable update interrupt
  TIM1->CNT += (65536UL-TCLKS);       // initial load
  TIM1->CR1 = TIM_CR1_CEN;            // enable timer

  NVIC->ISER[0] = (1 << TIM1_UP_TIM16_IRQn);  // enable TIM1 int in NVIC

  while (1)
  {
    TIM1->DIER &= ~TIM_DIER_UIE;      // disable update interrupt
    GPIOC->ODR ^= (1<<9);             // toggle green LED
    TIM1->DIER |= TIM_DIER_UIE;       // re-enable update interrupt

    delay(80000);
  }
}

Oops - Another Shared Resource, Another Source of Bugs!

Remember all that nasty interrupt-related corruption we learned about?  Much the same kind of thing can happen with certain timer registers.  Remember, the timer register keeps on timing (counting up or sometimes down) while user code is running.  You can think of the timer register as a data variable, which can be accessed by two different independent processes - the timer hardware, and the user code.  Once again we have a potential for data corruption.  Imagine an 8-bit device like the AVR reading the 16-bit timer register:

  • Timer register contains 0x00FF
  • User code reads low byte of timer register (0xFF)
  • Timer ticks, now timer register contains 0x0100
  • User code reads high byte of timer register (0x01)

Now the user code has a corrupt timer register value of 0x01FF, because the timer hardware essentially "interrupted" the 2-stage timer register load process and changed the timer register data between those two stages.  Rats and radishes!  The same thing can also happen when reading an input capture register (suppose a new input capture event occurs during the 2-stage capture register load), and a similar problem can happen when updating a compare register in two stages.  All the problems in this case are caused by the fact that the uC must access dynamically changing registers using multi-stage accesses.  If the uC can access those registers in a single access (8-bit timer for 8-bit uC, 32-bit timer for 32-bit uC), the problem does not exist. 

The AVR designers understood this and provided a fix.  For all 16-bit hardware registers where such corruption can occur, the AVR hardware has a special trick.  When reading e.g. the timer register, a read of the low byte of the register actually reads both bytes at the same time (does a full 16-bit atomic transfer from the hardware register, thus preventing any corruption of the read data).  The other, high, byte is saved in a temporary register, and when the user code reads the high byte address, it is the value in the temp register that is returned, not the actual high byte of the timer register (which may have changed since reading the low byte).  In the same manner, writing to the high byte of such a register saves the data in a temp register, and then writing to the low byte causes a full 16-bit atomic transfer into the hardware register, using the written low byte and the high byte saved in the temp register.

A good AVR compiler will know this sequence and apply it automatically.  When programming in ASM, or otherwise working with the register as 2 bytes rather than as a 16-bit value, it is up to you to remember the sequence: read low then high, write high then low.

Getting Fancy

Real timers can do much more than what we have just discussed.  To add these capabilities they come with additional types of registers.  One type of register that just about all timers will have is called a "match" or "compare" register.  As the timer is counting up, when its value equals the value in a compare register another flag, the match or compare flag, is set, different than the overflow flag.  Just like the overflow flag, this flag can be inspected with code, or it can be used to generate an interrupt.  And, very importantly, the chip designers can include as many compare registers as they want in a single timer, all acting independently.  In addition, compare registers can generally be mapped to an output pin so that compare matches can set, clear or toggle the output.  This functionality will be discussed more in a future PWM chapter.

The other type of register generally found is an "input capture" register.  When an external input or other signal goes active, the value of the timer at that instant is saved in the input capture register.  This lets us take a snapshot of the timer value based on some event while the timer continues to run.  Again, the chip designer can include as many capture registers as they want in a single timer, each with a separate capture trigger signal.

It should be clear that all compare and capture registers are the same size (same number of bits) as the timer register itself.  A 8-bit timer will have 8-bit compare and capture registers, and so forth.  It should also be pointed out that in the STM32 in particular, the same register can be configured to be either a compare register or an input capture register.

The -1 Rule

When loading timer registers with timing values, it is critical to understand how a timer compare works - that is, how a value in a timer (or prescaler) register is compared to the running timer (or prescaler) count.  For a compare value of N, the compare event only takes place at the next timer clock after the timer has reached N.  Thus for example, for N=3, if you are using the compare event to reset the timer you will have the following sequence:

0, 1, 2, 3, (compare happens here) 0, 1, 2, 3, (compare happens here)...

Note that the timer actually is counting 4 steps before resetting to 0 and starting over.  This is just the way counters work, including the counters in timers.  They can't count up to the next value and also do a compare for that next value on the same clock.  Always read the datasheet, of course, but in general to count N timer clocks you need to load the compare register with the value of N-1.  That is the "-1 Rule".  This applies to any user-configurable prescaler compare registers too.  For example, to get a prescale of 64 on the STM32 devices, we must load a value of 63 into the timer prescale register.

LED Blink Using Compare Register - AVR Version

This time we set up the AVR timer for early rollover using a compare register (AVR calls this CTC mode).  Now our interrupt is based on the compare event rather than the rollover event, and in the ISR all we have to do is toggle the LED - no timer fiddling is required.

// AVR_TIM2
// toggle PB0 LED with software delay
// toggle PB1 LED in compare (CTC) ISR
// AVR clock is 8MHz

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU       8000000UL  // 1MHz
#define PRESCALE    64
#define PERIOD      250     // milliseconds - 1/4 second delay
#define T1_CLKS     ((F_CPU/PRESCALE*PERIOD)/1000)

ISR(TIMER1_COMPA_vect)
{
  PORTB ^= (1<<PB1);               // toggle PB1
}

void delay(volatile uint32_t d)
{
  while (d-- != 0)
    ;
}

int main(void)
{
  DDRB = 3;                         // PB0, PB1 outputs
  
  OCR1A = T1_CLKS-1;                // must load with count-1
  TIMSK = (1<<OCIE1A);              // enable compare interrupt
  TCCR1A = 0;                       // CTC mode
  TCCR1B = (1<<WGM12) | (3<<CS10);  // CTC mode, /64 prescaler
  
  sei();
  
  while(1)
  {
    cli();
    PORTB ^= (1<<PB0);
    sei();
    
    delay(80000);
  }
}

LED Blink Using Compare Register - STM32 Version

For the STM32 we must use a special-purpose compare register, called the auto-reload register, to roll the timer over to 0.  Also, for the STM32, our interrupt source is not a compare but an overflow, as in the first STM32 timer program.  That's just the way the STM32 timers work - not better or worse, just different.

// STM32_TIM2
// toggle LED on PC9 using software delay
// toggle LED on PC8 using timer 1 with auto reload
// Default 8 MHz internal clock

#include <stm32f10x.h>

#define F_CPU			8000000UL	// 1MHz
#define PRESCALE		64
#define PERIOD			250	// milliseconds - 1/4 second delay
#define TCLKS			((F_CPU/PRESCALE*PERIOD)/1000)

void TIM1_UP_TIM16_IRQHandler(void)
{
  GPIOC->ODR ^= (1<<8);       // toggle blue LED
  TIM1->SR = ~TIM_SR_UIF;     // clear update int flag (write 0)
}

void delay(volatile uint32_t d)
{
  while (d-- != 0)
    ;
}

int main(void)
{
  RCC->CFGR = 0;                      // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // enable PORTA

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // enable PORTC
  GPIOC->CRH = 0b0010 | (0b0010 << 4); // CNF=0, MODE=2 (2MHz output) PC8,PC9

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // enable Timer1

  TIM1->PSC = PRESCALE - 1;
  TIM1->DIER = TIM_DIER_UIE;          // enable overflow interrupt
  TIM1->CCMR1 = 0;          // chan 1 is output
  TIM1->ARR = TCLKS - 1;
  TIM1->CR1 = TIM_CR1_CEN;            // enable timer

  NVIC->ISER[0] = (1 << TIM1_UP_TIM16_IRQn); // enable TIM1 int in NVIC

  while (1)
  {
    TIM1->DIER &= ~TIM_DIER_UIE;      // disable update interrupt
    GPIOC->ODR ^= (1<<9);             // toggle green LED
    TIM1->DIER |= TIM_DIER_UIE;       // re-enable update interrupt

    delay(80000);
  }
}

More About Compare Registers

One of the things that a compare register can be configured to do is to cause the timer to roll over to zero upon a match.  In some cases only one special compare register can cause such rollover - the STM32 has such registers and calls them "auto-rollover" registers.  This "early rollover" lets us create a timer of any length or period less than the maximum timer period, but one that does not suffer the weakness of the timer reload method discussed above.  When we have a compare register rollover, the timer continues to run from 0 regardless of how long it takes any compare ISR code to run.  The timer neither knows nor cares about how long it takes our ISR to run.  It simply goes on generating exact time intervals and interrupts.  Even if one timer ISR entry is delayed due to other ISRs running or global interrupts being disabled for a time, the next one will happen on time.  There will never be any long-term slippage of the timer interrupts, which is something we cannot guarantee with the earlier timer reload technique.  Now we finally have an adjustable source of completely stable timer ticks, thanks to the compare register.

Instead of configuring a timer to roll over on a compare, we can also just generate a compare interrupt while letting the timer continue to run past the compare value.  One interesting and useful thing we can do with compare registers in such a case is to reload them in the compare ISR.  In this case it is crucial to not enable the compare rollover option as we did above, but to let the timer run its full natural period of 2^8, 2^16 or 2^32 timer clocks, and then overflow.  In the compare ISR, we simply add our desired interval value to the current compare register value and that becomes our next compare register value.  It's as simple as this:

Compare_reg += TIME_INTERVAL;  // next compare will happen TIME_INTERVAL clocks after this one

Note that it does not matter how many counts the timer has advanced since the last compare (as long as it is less than TIME_INTERVAL!).  Even if this compare ISR runs late for whatever reason, the next one will be back on schedule.  This technique (sometimes called "leapfrogging" for rather obvious reasons) gives exactly the same rock-solid timing as the zero-on-compare method, but it has even more versatility because one can use multiple compare registers to generate multiple independent timing events, such as:

ISR(COMP1A_vect)  // this ISR runs once every 3000 timer ticks
{
  OCR1A += 3000;
  ...
}

ISR(COMP1B_vect)  // while this ISR runs once every 725 ticks
{
  OCR1B += 725;
  ...
}

This generates two unrelated but stable timing loops in a very simple way.

LED Blink Using Leapfrogging - AVR Version

This time we have two compare interrupts and each ISR advances (leapfrogs) the associated compare register and also sets a flag.  Background code checks for both flags and toggles the appropriate LED when it sees a flag set.  Note that PORTB is no longer shared between background code and ISR, so no disabling and re-enabling of interrupts is necessary.  Also note that the background code is responsible for clearing any flags set by ISRs.

// AVR_TIM3
// toggle PB0 LED with one leapfrog delay
// toggle PB1 LED with a different leapfrog delay
// AVR clock is 8MHz

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU       8000000UL  // 1MHz
#define PRESCALE    64
#define PERIOD      250     // milliseconds - 1/4 second delay
#define T1_CLKS     ((F_CPU/PRESCALE*PERIOD)/1000)

volatile uint8_t flag0 = 0;
volatile uint8_t flag1 = 0;

ISR(TIMER1_COMPA_vect)
{
  flag0 = 1;
  OCR1A += T1_CLKS;
}

ISR(TIMER1_COMPB_vect)
{
  flag1 = 1;
  OCR1B += (T1_CLKS + 1000);    // slightly longer delay than flag0
}

int main(void)
{  
  DDRB = 3;                           // PB0, PB1 output
  
  OCR1A = 31250;                      // must load with count-1
  OCR1B = 32000;
  TIMSK = (1<<OCIE1A) | (1<<OCIE1B);  // enable overflow interrupts
  TCCR1A = 0;                         // normal mode
  TCCR1B = (3<<CS10);                 // normal mode, /64 prescaler
  
  sei();
  
  while(1)
  {
    if (flag0)
    {
      PORTB ^= (1<<PB0);
      flag0 = 0;
    }
    if (flag1)
    {
      PORTB ^= (1<<PB1);
      flag1= 0;
    }
  }
}

LED Blink Using Leapfrogging - STM32 Version

Similar to the AVR version, using two compare registers and a free-running timer, but both compare registers vector through the same interrupt, so in the ISR a test must be made to determine which compare has generated the interrupt.

// STM32_TIM3
// toggle LED on PC9 using leapfrog delay
// toggle LED on PC8 using different leapfrog delay
// Default 8 MHz internal clock

#include <stm32f10x.h>

#define F_CPU			8000000UL	// 1MHz
#define PRESCALE		64
#define PERIOD			250	// microseconds - 1/4 second delay
#define TCLKS			((F_CPU/PRESCALE*PERIOD)/1000)

volatile uint8_t flag1 = 0;
volatile uint8_t flag2 = 0;

void TIM1_CC_IRQHandler(void)
{
  if (TIM1->SR & TIM_SR_CC1IF)  // CC1 match?
  {
    flag1 = 1;
    TIM1->CCR1 += TCLKS;        // leapfrog to next interrupt time
    TIM1->SR = ~TIM_SR_CC1IF;   // clear CC1 int flag (write 0)
  }
  if (TIM1->SR & TIM_SR_CC2IF)  // CC2 match?
  {
    flag2 = 1;
    TIM1->CCR2 += (TCLKS+1000); // leapfrog to next interrupt time
    TIM1->SR = ~TIM_SR_CC2IF;   // clear CC2 int flag (write 0)
  }
}

int main(void)
{
  RCC->CFGR = 0;                       // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;  // enable PORTA

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;  // enable PORTC
  GPIOC->CRH = 0b0010 | (0b0010 << 4); // CNF=0, MODE=2 (2MHz output) PC8,PC9

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;  // enable Timer1

  TIM1->PSC = PRESCALE - 1;
  TIM1->DIER = TIM_DIER_CC2IE | TIM_DIER_CC1IE; // enable 2 CC interrupt channels
  TIM1->CCMR1 = 0;                     // chan 1 is output
  TIM1->CR1 = TIM_CR1_CEN;             // enable timer

  NVIC->ISER[0] = (1 << TIM1_CC_IRQn); // enable TIM1 int in NVIC

  while (1)
  {
    if (flag1)
    {
      GPIOC->ODR ^= (1<<9);            // toggle green LED
      flag1 = 0;
    }
    if (flag2)
    {
      GPIOC->ODR ^= (1<<8);            // toggle blue LED
      flag2 = 0;
    }
  }
}

But Surely That Can't Work!

Neat, but you're concerned about what happens when the timer or the compare register addition rolls over, right?  You should be.  Assuming a 16-bit timer and 16-bit OCR1 registers, you can't just keep adding that 3000 to a 16-bit register forever without an overflow.  When we get up to 63,000 (adding 3000 21 times) and then add 3000 again, we go to our handy calculator program and see 0xF618 (=63,000) + 0xBB8 (=3000) = 0x101D0, or, with only 16 bits to hold the result, 0x1D0 (=464).  The timer register overflowed, now what?  Now we get lucky, or so it seems at first glance.  Consider how many more counts from 63,000 it will take for the timer to overflow: 65,536 - 63,000 = 2536, so we will have 2536 more counts, then the timer will roll over, then, if we have loaded 464 into our compare register (the result of the overflowed addition of 3000), we will have another 464 counts before our compare match.  But 2536+464 = 3000, exactly the interval we wanted.  We get this fortuitous result because the binary math involved in the 16-bit timer overflow is exactly the same binary math involved in our adding 3000 to 63,000 in a 16-bit compare register.  Remember, a timer is just a counter, so our timer register is simply adding 1 to itself 3000 times, which gives exactly the same result as adding 3000 to our compare register 1 time.  In both cases, the overflow just takes care of itself.  I had a real "I'll be darned!" moment when I first understood that.

We have shown fixed intervals in all of the examples so far, but of course the intervals can be variable, or can be an assortment of fixed intervals, not just one.  All that matters is that the new compare register value must be "further out" than the actual timer register.  If you just had a compare interrupt at timer=100, and you foolishly wrote a very long ISR, and now the timer is at 234 by the time your ISR gets around to loading the compare register with a new value of 200, well, you're hosed.  At the very least, you need to load your compare register with a value further out than the timer will be when the ISR finishes.  Hence, write short ISRs!

Counting Ticks

We'll end up this chapter on timers by setting up a timer to generate 10ms ticks, and using those ticks to blink our two LEDs.  What we are doing now, in essence, is extending the hardware counting found in the timer and its prescaler into software.  That is, we are now doing some of our timer clock counting in the timer hardware, and the rest of it in software.  This lets us extend our timing capabilities to any possible interval you would ever need.  Having an timer-driven system tick is such a common requirement that all ARM Cortex parts have, as part of the ARM spec, a separate 24-bit system tick timer.  This is the timer one would generally use for creating a system tick, but to stay in line with the other examples above we will continue to use the STM32 Timer 1.

Our programs will be similar to our Compare & Clear programs, but this time we will set up the timer for a 10ms tick, and on every tick set a flag to signal the background code.  The background code will spend just about all of its time watching for that flag, and when it finds the flag set it will count another tick.  Every 24 (or 25) of those ticks the background code will toggle one (or the other) LED, giving us two blinking LEDs with slightly different blink frequencies.  This sharing of work between ISR and background code is a very common paradigm, which is why we're introducing it now instead of e.g. just counting ticks inside the ISR and toggling the LEDs inside the ISR, which might seem the more obvious approach.

Note also that by using an 8-bit flag, set in the ISR and cleared in background code but never involved in a RMW sequence, there is no possibility of flag corruption.  You want to make it a habit to confirm lack of corruption with any such shared data.

LED Blink Using Ticks and Counting - AVR Version

This time the timer is set up to deliver 10ms ticks to the background code.  The background code maintains two tick counters, one for each LED, and toggles the appropriate LED when its counter reaches a specified value.  Again we have no PORTB sharing in this method, so no need to worry about possible PORTB corruption.

// AVR_TIM4
// toggle PB0 LED after 24 10ms timer ticks
// toggle PB1 LED after 25 10ms timer ticks
// AVR clock is 8MHz

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU       8000000UL  // 1MHz
#define PRESCALE    64
#define PERIOD      250     // milliseconds - 1/4 second delay
#define T1_CLKS     ((F_CPU/PRESCALE*PERIOD)/1000)

volatile uint8_t tick = 0;

ISR(TIMER1_COMPA_vect)
{
  tick = 1;
}

int main(void)
{
  uint8_t tick_cnt0 = 0;
  uint8_t tick_cnt1 = 0;
  
  DDRB = 3;                         // PB0,PB1 output
  
  OCR1A = 1250-1;                   // must load with count-1
  TIMSK = (1<<OCIE1A);              // enable overflow interrupt
  TCCR1A = 0;                       // CTC mode
  TCCR1B = (1<<WGM12) | (3<<CS10);  // CTC mode, /64 prescaler
  
  sei();
  
  while(1)
  {
    if (tick)
    {
      if (++tick_cnt0 >= 24)        // 240ms
      {
        PORTB ^= (1<<PB0);
        tick_cnt0 = 0;
      }
      if (++tick_cnt1 >= 25)        // 250ms
      {
        PORTB ^= (1<<PB1);
        tick_cnt1 = 0;
      }
      tick = 0;
    }
  }
}

 

LED Blink Using Ticks and Counting - STM32 Version

Again this STM32 program is very similar to the AVR version.

// STM32_TIM4
// Blink LED on PC9 using 24 10ms timer ticks
// Blink LED on PC8 using 25 10ms timer ticks
// Default 8 MHz internal clock

#include <stm32f10x.h>

#define F_CPU			8000000UL	// 1MHz
#define PRESCALE		64
#define TICK_CLKS       ((F_CPU/PRESCALE*10)/1000)  // 10ms

volatile uint8_t tick = 0;

void TIM1_UP_TIM16_IRQHandler(void)
{
  tick = 1;
  TIM1->SR = ~TIM_SR_UIF;     // clear update int flag (write 0)
}

int main(void)
{
uint8_t tick_cnt1 = 0;
uint8_t tick_cnt2 = 0;

  RCC->CFGR = 0;                       // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;  // enable PORTC
  GPIOC->CRH = 0b0010 | (0b0010 << 4); // CNF=0, MODE=2 (2MHz output) PC8,PC9

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;  // enable Timer1

  TIM1->PSC = PRESCALE - 1;
  TIM1->DIER = TIM_DIER_UIE;           // enable overflow interrupt
  TIM1->CCMR1 = 0;                     // chan 1 is output
  TIM1->ARR = TICK_CLKS - 1;
  TIM1->CR1 = TIM_CR1_CEN;             // enable timer

  NVIC->ISER[0] = (1 << TIM1_UP_TIM16_IRQn); // enable TIM1 int in NVIC

  while (1)
  {
    if (tick)
    {
      if (++tick_cnt1 >= 24)
      {
        GPIOC->ODR ^= (1<<9);       // toggle green LED
        tick_cnt1 = 0;
      }
      if (++tick_cnt2 >= 25)
      {
        GPIOC->ODR ^= (1<<8);       // toggle blue LED
        tick_cnt2 = 0;
      }
      tick = 0;
    }
  }

Next

The next tutorial chapter on timers will show a few more very useful ways to use a timer, and then will explore ways of getting useful work done using a simple time-and-event driven scheduler.  However, before we get to that we have to make a very important detour...



The 2024 Embedded Online Conference
[ - ]
Comment by jms_nhSeptember 30, 2013
well-written article! I would make one minor quibble: "Larger timers (more bits) are more versatile, but they are also more expensive in terms of chip area." Technically that's true, but timer/counters of normal lengths (8/16/32 bits) use so little chip area that they're essentially free; the core / flash / analog circuitry / IC pads / etc are much larger.
[ - ]
Comment by Andrei6March 29, 2015
Hello again!
Could anyone please tell me why my compiler(keil uVision) can't comppile "0b0010 | (0b0010 << 4);"
Even with the previous examples, I had to replace "(0b0010 << 4)" with "32" in order for it to work, but I don't know why!
If I erase "0b" it compiles fine, without errors, but it simply doesn't work.
Please, I have asked this on other articles of this site too, but I can't find an answer!
Thanks in advance!

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: