4
\$\begingroup\$

I'm trying to implement a global timer, so that I can call time_us() anywhere in program and the function will return microseconds since program start. I've done this using TIMER2 (8-bit clock) and interrupt on overflow, and counting the overflows:

volatile uint64_t _time_overflow_cnt;

void time_reset(){
    TCCR2A=0; // no pin output,
    TCCR2B=(1<<CS21); // prescaler=8
    TIMSK2=1<<TOIE2; // interrupt on overflow (256 cycles)
    sei();
    TCNT2=0; // reset timer to 0
    _time_overflow_cnt=0;
}

ISR(TIMER2_OVF_vect){
    _time_overflow_cnt++;
}

uint64_t time_us(){
    return (_time_overflow_cnt*256 + TCNT2)/(F_CPU/8/1000000ULL);
}

However, this code has a race condition, which is in my case quite noticeable - if we rewrite this code into assembly, we see that the time_us is quite lengthy (64-bit calculations take some time) and if TCNT2 overflows during this time, the calculations is skewed by whole 256 cycles. I tried turning interrupts off before and turning them back on after calculation, but this does not help - it only secures the overflow counter, while the TCNT2 keeps increasing. I also tried saving overflow counter, then saving TCNT2, checking if the former changed in between, and a few other 'solutions' similar to this one, but all off them had some TOCTOU (Time Of Check to Time Of Use) problems. Have you got a better idea for implementation?

\$\endgroup\$
2
  • \$\begingroup\$ Have you tried loading both values into registers first? \$\endgroup\$ Commented Jul 21, 2015 at 5:09
  • \$\begingroup\$ Yeah, if you mean: uint64_t old_cnt=_time_overflow_cnt; uint8_t old_tcnt2=TCNT2; return whatever; - it has the same issues, as TCNT2 may overflow between those assignments. \$\endgroup\$
    – akrasuski1
    Commented Jul 21, 2015 at 5:11

2 Answers 2

3
\$\begingroup\$

Just make the retrieval atomic.

#include <util/atomic.h>

 ...

uint64_t time_us(){
    register uint8_t tcnt2
    register uint64_t ovcnt;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
    {
        tcnt2 = TCNT2;
        ovcnt = _time_overflow_cnt;
    }
    return (ovcnt*256 + tcnt2)/(F_CPU/8/1000000ULL);
}
\$\endgroup\$
5
  • \$\begingroup\$ Thanks, I totally forgot about the atomic blocks! This should solve the problem. \$\endgroup\$
    – akrasuski1
    Commented Jul 21, 2015 at 5:31
  • 1
    \$\begingroup\$ I tried turning interrupts off before and turning them back on after calculation, but this does not help - it only secures the overflow counter, while the TCNT2 keeps increasing The ATOMIC_BLOCK does the same thing (disabling/enabling the interrupt), so how's the code different from the OP's? \$\endgroup\$
    – m.Alin
    Commented Jul 21, 2015 at 7:29
  • \$\begingroup\$ @m.Alin: It provides a definite ordering to the retrieval of the values; even if TCNT2 keeps incrementing, we've already retrieved it plus we've blocked it from changing the overflow count. \$\endgroup\$ Commented Jul 21, 2015 at 9:48
  • \$\begingroup\$ Hmmm, I accepted this answer previously because it seemed to help in my case... I totally forgot that in the meantime I changed the clock to another prescaler as a workaround to make race condition less frequent. The bottom line is that this solution does not work. I will post a working one in a minute. \$\endgroup\$
    – akrasuski1
    Commented Jul 22, 2015 at 0:29
  • \$\begingroup\$ @IgnacioVazquez-Abrams There's still a race condition here if the following sequence occurs: 0) Interrupts are disabled to enter the atomic block 1) TCNT2 overflows 2) TCNT2 is sampled into the tcnt2 variable 3) _time_overflow_cnt is read but is incorrect because the ISR which updates it is blocked due to the atomic section. We can end up returning a value that appears to have jumped backwards in time. \$\endgroup\$
    – Jon L
    Commented Mar 28, 2017 at 16:04
2
\$\begingroup\$

I could not think of a working solution myself, so I searched the Internet a lot, and after a while, I stumbled upon this blog post: http://www.electricrcaircraftguy.com/2014/02/Timer2Counter-more-precise-Arduino-micros-function.html#.Va7hMh9h3dI

It reimplements Arduino's micros() function on Arduino platform, because the writer wanted to achieve higher precision of the timer. Although the code was written in Arduino language, using calls to non-AVR functions, I translated it to pure C, and it seemed to work fine. Below is the code that works for me (comments are author's; according to them, he had the exact same issues formerly - overflowing TCNT2 while in the function) :

uint64_t time_us(){

    cli();
    uint8_t tcnt2_save = TCNT2; //grab the counter value from Timer2
    uint8_t flag_save = IS_HIGH(TIFR2,0); //grab the timer2 overflow flag value
    if (flag_save) { //if the overflow flag is set
        tcnt2_save = TCNT2; //update variable just saved since the overflow flag could have just tripped between previously saving the TCNT2 value and reading bit 0 of TIFR2.
                            //If this is the case, TCNT2 might have just changed from 255 to 0, and so we need to grab the new value of TCNT2 to prevent an error of up
                            //to 127.5us in any time obtained using the T2 counter (ex: T2_micros). (Note: 255 counts / 2 counts/us = 127.5us)
                            //Note: this line of code DID in fact fix the error just described, in which I periodically saw an error of ~127.5us in some values read in
                            //by some PWM read code I wrote.
        _time_overflow_cnt++; //force the overflow count to increment
        TIFR2 |= 0b00000001; //reset Timer2 overflow flag since we just manually incremented above; see datasheet pg. 160; this prevents execution of Timer2's overflow ISR
    }
    uint64_t ticks = _time_overflow_cnt*256 + tcnt2_save; //get total Timer2 count
    sei(); //allow interrupts again
    return ticks/(F_CPU/8/1000000ULL);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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