6
\$\begingroup\$

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.

\$\endgroup\$
4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ doing the update in the main loop based on a flag is a good idea... I like it. Given that TWI is asynchronous, any other blocking ISR will obstruct a TWI handler with some probability right? \$\endgroup\$ – vicatcu Dec 3 '12 at 15:17
  • \$\begingroup\$ I am not that familiar with TWI, so i don't know how fast you have to react and how often an interrupt may occur. But i for myself consider allowing nested interrupts kind of a bad practice since it is almost always avoidable. It only encourages to get lazy and not pay attention to the overall behavior/timing of the code. I would try to estimate the time window in that the TWI handler has to be executed and then check if the other ISRs execution time is within acceptable limits. Remember that interrupt priority can be important if you have lots of other fast recurring interrupts. \$\endgroup\$ – Rev1.0 Dec 3 '12 at 16:11
  • \$\begingroup\$ When I say 'obstruct' I don't mean 'interrupt in progress'... rather I mean 'cause it to be missed entirely.' There is a chance, albeit a small one, that an I2C data received interrupt could happen while a Timer ISR is in progress. If the Timer ISR disables interrupts, that I2C data received event will never be handled, right? \$\endgroup\$ – vicatcu Dec 4 '12 at 17:18
  • \$\begingroup\$ When an ISR is entered, the global interrupt enable flag is cleared, but this doesn't suppress all other interrupts from being "detected". The interrupt flags will still be set, but the calls to the interrupt service routine will be DELAYED until the global interrupt flag is re-enabled. If multiple flags are set during ISR execution, the handlers are executed in order of priority. This means that you won't miss any interrupt as long as you give them a chance to be processed before they occur the next time. \$\endgroup\$ – Rev1.0 Dec 5 '12 at 13:37
  • 1
    \$\begingroup\$ I was referring to this behaviour "...if one or more interrupt conditions occur while the Global Interrupt Enable bit is cleared, the corresponding Interrupt Flag(s) will be set and remembered until the Global Interrupt Enable bit is set, and will then be executed by order of priority." I wasn't aware of the "second type of interrupts" mentioning in the datasheet, interesting. I will read up on that. Maybe its straightforward and just the nature of how some interrupts are triggered. \$\endgroup\$ – Rev1.0 Dec 6 '12 at 7:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.