Electronic – Time Base ISR Concurrency

avrcfirmwareinterrupts

This is sort of a "classic" problem, and I think I have a solution, but I want to vet it with this community. I am building a project using the ATtiny88 microcontroller, and I'm programming in avr-gcc. I need it to handle the following interrupts:

  • TWI (I2C)
  • Timer0 Overflow
  • Timer1 Capture

I want to use the Timer0 Overflow to maintain a 32-bit millisecond timestamp (similar to Arduino's millis() concept). For my application I can't have anything get in the way of the TWI interrupt because my ATtiny88 is acting as a TWI slave, and as such I can't ever clear interrupts. The Timer ISRs both have to be declared NO_BLOCK.

Since the ATtiny88 is an 8-bit processor, access to variables that are wider than 8-bits are certain to take multiple cycles to complete. The way the Arduino core handles this dilemma is by guarding access to its internal 32-bit timer0_millis variable with a cli() inside the millis() function. That approach is unpalatable to me because it means I might miss a TWI interrupt because I call millis(). I don't care about occasionally missing a "tick" of the timebase because of a TWI interrupt being handled.

So I wrote timebase.h / timebase.c in hopes of circumventing this problem at the expense of a little extra storage, by double buffering.

timebase.h

/*
 * timebase.h
 *
 *  Created on: Dec 3, 2012
 *      Author: vic
 */

#ifndef TIMEBASE_H_
#define TIMEBASE_H_

// timer 0 is set up as a 1ms time base
#define TIMER0_1MS_OVERFOW_PRESCALER 3     // 8MHz / 64 = 125 kHz
#define TIMER0_1MS_OVERFLOW_TCNT     131   // 255 - 131 + 1 = 125 ticks

void timebase_init();
uint32_t timebase_now();

#endif /* TIMEBASE_H_ */

timebase.c

/*
 * timebase.c
 *
 *  Created on: Dec 3, 2012
 *      Author: vic
 */
#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "timebase.h"

// double buffered timestamp
volatile uint32_t timestamp_ms_buf[2] = {0, 1};
volatile uint8_t  timestamp_ms_buf_volatile_idx = 0;

void timebase_init(){
    // set up the timer0 overflow interrupt for 1ms
    TCNT0  = TIMER0_1MS_OVERFLOW_TCNT;
    TIMSK0 = _BV(TOIE0);

    // start timer0
    TCCR0A = TIMER0_1MS_OVERFOW_PRESCALER;
}

// fires once per millisecond, don't block
// can't miss TWI interrupts for anything
ISR(TIMER0_OVF_vect, ISR_NOBLOCK){
    TCNT0 = TIMER0_1MS_OVERFLOW_TCNT;

    // modify the volatile index value
    timestamp_ms_buf[timestamp_ms_buf_volatile_idx] += 2;

    // change the volatile index
    timestamp_ms_buf_volatile_idx = 1 - timestamp_ms_buf_volatile_idx; // always 0 or 1
}

uint32_t timebase_now(){
    uint8_t idx = timestamp_ms_buf_volatile_idx; // copy the current volatile index
    return timestamp_ms_buf[1 - idx];            // return the value from the non-volatile index
}

My question is, does the code I've written here effectively solve the problem I've described? Have I successfully implemented atomic access to the timebase without clearing interrupts? If not, why not, and how can I achieve my goals.

Best Answer

Judging from a quick look at it, the double buffering looks like a good approach. However, I believe it can still happen that you get an "invalid" value returned. Theoretically the T0 interrupt could fire multiple times while you access the timestamp in the timebase_now() function (if execution is delayed >1ms by another ISR) and would make your "double" buffering useless.

Are you sure it is even required to make your Timer0 ISR non-blocking? Since the hardware is handling all the low level TWI functions and the max data transfer speed is 400kHz, there should be enough time to handle TWI data. Updating the 4-byte timestamp variable only takes a few clock cycles. On what assumption are you expecting to loose TWI interrupts?

You say that timestamp accuracy is not critical, so another solution could be to just set a flag (or 1-byte counter) in the T0 ISR and handle updating the timestamp in the main loop. However, this only works if timebase_now() is not supposed to be callable from within any ISR.