Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I avoid this off-by-one error when adding an offset to a prescaled hardware timer?

I am writing a microcontroller interrupt that needs to add an offset to one of its hardware timers. However, due to the way the timer prescaler works, the naive approach can introduce an off-by-one error depending on the timing of the interrupt execution relative to the prescaler clock.

timing diagram of ISR off-by-one error

I am using timer 1 on an ATmega328P (= arduino) for this. I have it set up in normal mode with a /8 prescaler, and am using the timer capture interrupt to trigger this; the goal of the interrupt is to set the timer to overflow exactly period cycles after the event that triggers the input capture (in case the trigger occurs during another interrupt or other situation in which interrupts are disabled).

(I'm abusing the PWM output to trigger two mains optotriacs at a variable AC phase offset, without needing to burn all the CPU time on it; the interrupt is triggered by a zero crossing detector on the mains phase.)

The code for the ISR would be something like this:

uint_16 period = 16667;

ISR(TIMER1_CAPT_vect){
    TCNT1 = TCNT1 - ICR1 - period + (elapsed counter ticks during execution);
}

The critical interval here is the one between when TCNT1 is read from and when it is then written to again.

As far as I know, there is no way to directly read the state of the prescaler, so I don't think it's possible to just apply a different offset based on the ISR timing.

I could just reset the prescaler before the ISR (GTCCR |= _BV(TSM); GTCCR |= _BV(PSRSYNC); GTCCR &= ~_BV(TSM);) to synchronize, but that still introduces a random offset to the timer that depends on the ISR timing.

Another approach I am considering is to use a timer to generate an interrupt synchronized with the prescaler. I'm already using both output compare registers on timer 1, but timer 0 shares the prescaler so it could be used. However, the timer interrupt execution could end up being deferred by another interrupt or 'cli' block, so this isn't guaranteed to work.

How can I write my interrupt to avoid this bug?

like image 561
AJMansfield Avatar asked Nov 09 '22 01:11

AJMansfield


1 Answers

If you write the ISR as

ISR(TIMER1_CAPT_vect){
    int counter = TCNT1 - ICR1 - period + 3;
    asm("nop");
    asm("nop");
    TCNT1 = counter;
}

the writing of TCNT1 should take place exactly 24 cycles after the register being read, thus at the same prescaler 'phase'. (The number of nop's may be adjusted if neccessary, e.g. due to variations between different microcontroller types). However, the solution is not able to take into account the change in prescaler 'phase' taking place between the setting of ICR1 and the reading of TCNT1.

like image 165
Terje D. Avatar answered Nov 15 '22 11:11

Terje D.