Gotcha in CLI-less reading of software-extended timer

Go To Last Post
10 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

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.

Last Edited: Fri. Oct 24, 2014 - 09:02 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Could you set a flag in your interrupt and then check it in get_timer()? I'd do it something like this:

volatile uint8_t timer_hi;
volatile uint8_t timer_ovf;

ISR(TIMER0_OVF_vect)
{
    timer_ovf = 1;
    timer_hi++;
}

uint16_t get_timer( void )
{
    uint8_t lo, hi;
    do {
        timer_ovf = 0;
        lo = TCNT0;
        hi = timer_hi;
    } while (timer_ovf);
    return hi << 8 | lo;
}

 

Last Edited: Fri. Oct 24, 2014 - 05:20 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Are you really running so close to the edge that you can't disable interrupts for, what, 10 cycles?

If you are you have serious hardware issues and you will soon run into a show stopper IMHO.

You need a faster processor. That's the correct solution.

 

And like you already figured out, you already disable interrupts in your overflow IRQ.

 

The 'correct' way, since I can only guess at your requirements, is to have the timer overflow occur at a multiple of the time base you need. Then just count that time base. don't extent the base timer, just make a new timer tick.

 

for example, you want to do something every 30ms

have your timer overflow every 3ms and then count 10 of them, then reset your counter.

You will get jitter that way if your high priority interrupt collides with your low priority interrupt,  but it's pretty hard to avoid.

 

 

In order to completely remove all interrupts ( except the hi pri one ) is to read the low pri timer status reg, wait for the overflow, then subtract the current time t determine how long it's been since the overflow.

 

Total PITA

Keith Vasilakes

Firmware engineer

Minnesota

Last Edited: Fri. Oct 24, 2014 - 07:08 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

keith, thanks for another approach. With that approach even if the timer is more than 8 bits, I can use the first approach to reading, because now the low 8 bits will either be updating along with the others, or nothing will be updated, unlike with the described situation where the low 8 bits are always updating but the high might not be .

 

I will check more closely to see how much of a budget of interrupt disabling I have. I suppose one point of this thread was to warn others of this gotcha when attempting to implement a software-extended timer's read function without disabling interrupts.

 

christop, I think your solution suffers from the same problem. Consider how it would work if your read code was executing, but the overflow interrupt was disabled and the timer had just overflowed before your code began. You'd not see your own overflow flag set yet the high byte of the timer would be incorrect.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

blargg wrote:

christop, I think your solution suffers from the same problem. Consider how it would work if your read code was executing, but the overflow interrupt was disabled and the timer had just overflowed before your code began. You'd not see your own overflow flag set yet the high byte of the timer would be incorrect.

You're right. My version works only when the timer overflow interrupt is enabled, but the way I read your post it seemed like you didn't disable interrupts (and disabling interrupts doesn't stop TCNT0 from incrementing). So are you ever calling get_timer() with interrupts disabled?

 

I haven't looked through the datasheet regarding the overflow interrupt, but is it triggered exactly when the counter rolls over to 0? Or is there at least one instruction cycle in which code might read a 0 from TCNT0 before the overflow interrupt starts?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The more I think about this the more I think since you have a hammer every problem looks like a nail.

I think you need to start a new thread and tell us what the real problem is, what is so time critical that you can't wedge a simple IRQ in.

 

Then we can fix the problem rather than chasing optimization geese all day.

Keith Vasilakes

Firmware engineer

Minnesota

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

keith, I should have been clearer in my opening post. I could post a new one with my intent better stated. My main goal was to share the gotcha involved in this approach to a software-extended timer, not to get help solving the problem in my particular program; a cautionary tale of sorts to anyone else taking the approach I took. Mentioning the details of the particular project made this unclear; I just wanted to establish a little context. I hope to be clearer in future postings about things. I don't know about others, but when I'm coming up with solutions I explore various ways of doing things. Knowing that a particular approach has subtle problems is valuable. That was my goal.

 

I've updated the original post to clarify my intent.

 

keith v wrote:

The more I think about this the more I think since you have a hammer every problem looks like a nail.

 

Are you suggesting that I am not approaching my particular problem flexibly, only using a particular approach inappropriately? I hope this idea about what's going on in my mind doesn't detract from the main point of my post as a cautionary tale for anyone, even people who don't have only a hammer yet have still arrived at the (flawed) approach described.

 

The only thing that I can vaguely make out as a hammer is my preference for not disabling interrupts. I don't see that as a solution to every problem, as the mere lack of CLI doesn't solve a problem; I more see it as a way to avoid potential problems. I'm not highly experienced with interrupt timing and calculating worst-case overhead, thus would like to stick with my experience level in implementation. I know that if I miscalculate worst-case overhead, something worse than that might only occur very rarely so I might not see the problem. CLI-free code requires that I exercise my thinking, which I think is good for understanding things better, e.g. my tests that show how a continuous interrupt still gives a little time to mainline code.

 

I have researched the library I was using further and there is a small budget for disabling interrupts, and I am now considering and researching how to make use of this to simplify the code. Perhaps the biggest realization was that ISR_NOBLOCK is viable and keeps the disabled-interrupt time minimal.

 

christop, my point in the original post was that a repeatedly-firing higher-priority interrupt acts exactly like disabling lower-priority interrupts, but still allowing mainline code to run. Thus code which fails when the overflow interrupt is disabled will also fail when it's enabled but there is a repeatedly-firing higher-priority interrupt. That's the cautionary tale I was sharing.

Last Edited: Fri. Oct 24, 2014 - 09:18 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Ahhh ok, so I can just say there is no software solution to this problem :D

Keith Vasilakes

Firmware engineer

Minnesota

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The original post shows two solutions: SEI before RET(I) in the higher-priority interrupt handler, or the extra OVF flag check in the read function.

 

BTW, I edited my previous reply to you possibly before you made your quick reply just now, so you might not have seen my response to your sens that I was suffering from hammer-itis.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I think you need to start a new thread and tell us what the real problem is,

There is no need to start a new thread.  The OP is/has clarified things.

I would rather attempt something great and fail, than attempt nothing and succeed - Fortune Cookie

 

"The critical shortage here is not stuff, but time." - Johan Ekdahl

 

"Step N is required before you can do step N+1!" - ka7ehk

 

"If you want a career with a known path - become an undertaker. Dead people don't sue!" - Kartman

"Why is there a "Highway to Hell" and only a "Stairway to Heaven"? A prediction of the expected traffic load?"  - Lee "theusch"

 

Speak sweetly. It makes your words easier to digest when at a later date you have to eat them ;-)  - Source Unknown

Please Read: Code-of-Conduct

Atmel Studio6.2/AS7, DipTrace, Quartus, MPLAB, RSLogix user