CPU, Interrupted — Timers

Yesterday I mentioned that interrupts on an embedded microcontroller can cause you trouble. That’s not strictly true – it’s not the interrupt that causes trouble it’s the way you handle them. An awful lot of programmers don’t handle them correctly and they end up in a sticky mess. Before I can talk about interrupts though, we need a practical instance of them in use. Let’s begin then by looking at timers and their interrupts.

Pretty much all embedded microcontrollers include at least one timer peripheral. Let’s look at how we should safely handle such an interrupt.

I’ll use code for an AVR atmega8 here, but the principles are identical whatever your CPU. AVR timer0 is 8-bit and only counts upwards; it can generate an interrupt when it overflows, allowing us to detect the overflow.

uint8_t Timer_1ms = 0;

static void initTIMER0( void )
{
    // TCCR0      -     -     -       -     -    CS02  CS01 CS00
    // TCNT0    ------------------ TCNT0[7:0] --------------------
    // TIMSK    OCIE2 TOIE2 TICIE1 OCIE1A OCIE1B TOIE1  -   TOIE0
    // TIFR     OCF2  TOV2   ICF1  OCF1A  OCF1B  TOV1   -   TOV0

    // CLK/64 prescaler (see datasheet for table)
    TCCR0 = _BV(CS01) | _BV(CS00)
    // CS0[2:0] set the prescaler, you'll pick this according to your
    // project requirements, but we'll assume a 16 MHz crystal.  Divided
    // by 64 gives us a 250kHz increment rate.  If we then counted
    // every 250 ticks, we would have a millisecond timer.
    TCNT0 = (0x100 - 250);

    // Now imagine the timer ticking starting from this value...
    // 6   = 0 ticks   (0 us)
    // 7   = 1 tick    (4 us)
    // 8   = 2 ticks   (8 us)
    //        ...
    // 254 = 248 ticks (992 us)
    // 255 = 249 ticks (996 us)
    // 0   = 250 ticks (1000 us) OVERFLOW

    // Enable timer0 overflow interrupt
    TIMSK = _BV(TOIE0);
}

ISR(TIMER0_OVF_vect)
{
    // 1 ms has passed
    Timer_1ms++;
    // Reload timer
    TCNT0 = (0x100 - 250);
}

This isn’t very good. Note these faults:

  • The timer overflow interrupt is not the highest priority, and we have no way of guaranteeing that significant time hasn’t passed between the interrupt occurring and the ISR being called.
  • There is no way of knowing what the C compiler has added as preamble to the ISR. There can be other code before we even get to “Timer_1ms++”; i.e. more time passing.
  • We’ve ourselves made matters worse by reloading the timer after incrementing our millisecond timer.

In short: what if TCNT0 is not zero when we reset it? In that case we will get the following ticks:

TCNT0 == 254   =>   248 ticks since ISR (992 us)
TCNT0 == 255   =>   249 ticks since ISR (996 us)
TCNT0 == 0     =>   250 ticks since ISR (1000 us) OVERFLOW
TCNT0 == 1     =>   0   ticks since ISR (0 us)
TCNT0 == 2     =>   1   tick since ISR (4 us)
TCNT0 == 3     =>   2   ticks since ISR (8 us)
   ISR finally runs and resets TCNT0
TCNT0 == 6     =>   2   ticks since ISR (12 us)
   ...
TCNT0 == 254   =>   250 ticks since ISR (1000 us)
TCNT0 == 255   =>   251 ticks since ISR (1004 us)
TCNT0 == 0     =>   252 ticks since ISR (1008 us) OVERFLOW

Oops. Our 1ms timer is actually being incremented every 1008 microseconds. It might not sound like much but over time that will cause all our clocks to drift.

All of these can be eased by updating the ISR.

ISR(TIMER0_OVF_vect)
{
    // 1 ms has passed
    Timer_1ms++;
    // Reload timer
    TCNT0 -= 250;
}

Here we’ve subtracted rather than reset TCNT0. That means any ticks that happened before we were able to reload our timer aren’t lost. We’re still not right though. Let’s look at the assembly generated for the single C line that updates the timer register.

; TCNT0 -= 250;
in      r24, 0x32
subi    r24, 0xFA
out     0x32, r24

As is typical when we look at memory modifications in assembly, it is done as a read-modify-write cycle. Do you see the issue? There is a second process accessing the TCNT0 register (@0x32) – the timer hardware itself. Two processes with access to a single memory location screams “race condition” at us. What if, on entry, a tick is imminent?

; TCNT0 = 250 on entry
in      r24, 0x32
; R24 now 250; TCNT0 ticks and becomes 251
subi    r24, 0xFA
; TCNT0, now zero, minus 250 should be 1, but
; TCNT0 is loaded with R24-250, 0
out     0x32, r24

If a tick occurs during the ISR, after TCNT0 has been read then we miss a tick. Again over time our clock will drift from reality.

There is no fix for this when using a timer in this way, we have no way of preventing the race condition. The solution is to get help from the timer peripheral itself. Timer0 in the AVR is unsuitable, what we need is an automatic reload feature. On the AVR, the 16-bit timer, Timer1 supplies that via it’s so-called Clear Timer on Compare Match mode (CTC).

Let’s start again then.

#define F_CPU 16000000

uint8_t Timer_1ms = 0;

static void initTIMER1( void )
{
    // CTC mode (WGM1[3:0] = 0x04)
    TCCR1A = 0;
    TCCR1B = _BV(WGM12);
    // CLK/8 prescaler (see datasheet for table)
    TCCR1B |= _BV(CS11);

    // Automatic comparison register (16Mhz/8) is a 2 MHz tick, i.e. two
    // million ticks per second, or two thousand ticks per millisecond.
    // Note that we get one more tick than the value we put in this
    // register
    OCR1A = (F_CPU/8)/1000 - 1;

    // Enable timer1 match-A interrupt
    TIMSK = _BV(OCIE1A);
}

// Note the change of vector here from "overflow" to "match"
ISR(TIMER1_COMPA_vect)
{
    // 1 ms has passed
    Timer_1ms++;
}

Now we have no need to reset the timer register and the automatic reset means we have no race condition to deal with since only one process (the hardware peripheral) is accessing the counter register.

While we have created a good one millisecond timer, it’s quite impractical. Unfortunately this article has gone on much longer than I’d thought it would already, so I’ll delay the improvements to another time.

This entry was posted in FussyLogic and tagged , , , , , . Bookmark the permalink. Trackbacks are closed, but you can post a comment.

Post a Comment

You must be logged in to post a comment.