I want to share a subtle gotcha with a particular approach to a software-extended timer, so that others might avoid being caught by it. I encountered this the common software-based extended timer based on an 8-bit timer and overflow interrupt. In this approach I avoided disabling interrupts in the read function.
I failed to turn up anything about this particular issue, as most dicussions of software-extended timers involve disabling interrupts when reading the timer. Related topics: What is the right way to read a software extended timer?, Out of sequence time stamp....?, How to increase Atmega Timer resolution?
In this application I couldn't disable interrupts while reading the timer due to the latency that would add to another higher-priority interrupt, so I needed to use a different approach than the usual one of disabling them. My flawed approach was to read the low byte, the high byte, and then re-read the low byte again and if an overflow had occurred since the last read, repeat the process:
volatile uint8_t timer_hi; ISR(TIMER0_OVF_vect) { timer_hi++; } uint16_t get_timer( void ) { for ( ;; ) { uint8_t lo = TCNT0; uint8_t hi = timer_hi; if ( lo <= TCNT0 ) return hi<<8 | lo; } }
The loop handles the case where the timer overflowed and we therefore don't know whether we read the high byte before or after the overflow interrupt incremented it. If no overflow occurred between the two TIMER0 reads, we wouldn't get an inconsistent value. As far as I can tell this would be a solid approach, since the overflow would cause the interrupt in a timely manner.
But it's not solid. In some code that reads the timer repeatedly in a tight loop, it occasionally gets a value slightly before the previous reading, causing elapsed time calculations to report a large value and make it seem like a timeout occurred.
With the usual way of coding an interrupt handler with a RETI at the end, if an interrupt source generates repeated interrupts in a row, the mainline code is still able to execute one instruction between each interrupt. I've run test code that confirms this for the atmega8, atmega328, and attiny85. It's not unexpected since this behavior matches the descriptions of instructions and the delayed effect of SEI/RETI on enabling interrupts.
In this case, a higher-priority interrupt than timer overflow (e.g. INT0) being repeatedly triggered was effectively disabling all lower-priority interrupts, while still allowing mainline code to run (albeit quite slowly). This means that get_timer() cannot depend on the timer overflow interrupt being handled in a timely manner.
There may be an overflow interrupt pending (TIMER0 has already overflowed), which never gets handled across the TIMER0 read, timer_hi read, TIMER0 read sequence. The loop might see identical values for both TIMER0 reads, so never know that timer_hi still needs to be incremented. It can't simply check the overflow flag and increment the high byte itself, because reading the flag and timer_hi can't be done atomically, and thus it might read the flag just before the overflow interrupt is finally handled, then read timer_hi after it's been incremented, so think it needs to be incremented again.
One solution is to end the INT0 handler with a different sequence that ensures that the mainline code cannot run if there are repeated interrupts. I've tested that ending with SEI and then RETI (or just RET, since the I would now be superfluous) indeed stops mainline code (along with all other interrupt handlers) while the repeated lower-priority interrupts occur, because interrupts are enabled right after the RET(I) returns, and thus the INT0 handler can fire again before any mainline code has a chance to execute.
Another approach that I used (not wanting to modify the library that provides the INT0 handler) is to have get_timer() detect this unhandled overflow interrupt case and keep looping until it is handled:
uint16_t get_timer( void ) { for ( ;; ) { uint8_t lo = TCNT0; // be sure no unhandled overflow interrupt if ( !(TIFR & (1<<TOV0)) ) { uint8_t hi = timer_hi; if ( lo <= TCNT0 ) // no overflow between TCNT0 reads return hi<<8 | lo; } } }
As before, the two TCNT0 reads detect any overflow event between them. The added TIFR check detects an unhandled overflow interrupt persisting possibly from before the first TCNT0 read. If there is no unhandled overflow, then any further overflows that would invalidate the reading will be detected by the two TCNT0 reads and cause the loop to repeat.
As far as I can tell this fixed the issue. I'm bothered by how involved it is, but I can't think of any simplifications when taking this general route to solving the problem.
The following demo program, which can be run on a USBasp stick, outputs waveforms on PB2, PB3, and PB4 showing activity and how INT0 can prevent TIMER0_OVF from running, while still letting mainline code run:
avr-gcc -Wall -mmcu=atmega8 -DF_CPU=12000000 -Os demo.c avr-objcopy -j .text -j .data a.out avrdude -p atmega8 -c usbasp -U a.out
#include <avr/io.h> #include <avr/interrupt.h> int main( void ) { // Enable low-level INT0. Pin is tied low. MCUCR &= ~(3<<ISC00); // Enable TIMER0 OVF every 256 clocks TCCR0 = 1<<CS00; // no prescaling TIMSK |= 1<<TOIE0; sei(); DDRB |= 1<<4 | 1<<3 | 1<<2; for ( ;; ) { uint8_t pb = 0; // Toggle PB2 many times uint8_t n; for ( n = 50; n--; ) { pb ^= 1<<2; PORTB = pb; } // Toggle INT0 enabled GICR ^= 1<<INT0; } } // ISR_NAKED to reduce unnecessary compiler-generated save/restore // Toggle PB3 on INT0 interrupt ISR(INT0_vect, ISR_NAKED) { PORTB |= 1<<3; // __asm( "sei" ); // causes INT0 to hog all time __asm( "reti" ); } // Toggle PB4 on timer overflow interrupt ISR(TIMER0_OVF_vect, ISR_NAKED) { PORTB |= 1<<4; __asm( "reti" ); }
A logic trace of this running (wider view):
The top trace shows how quickly the main code is running. Near the middle it runs quickly while INT0 is disabled and just the timer overflow interrupt fires occasionally. Before/after that, INT0 is firing continuously and the main code is running far slower than normal. The overflow interrupt isn't firing at all during that time.
EDIT: clarified posting to be a cautionary tale, not a request to solve a particular problem in a particular program.