5

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?

AJMansfield
  • 4,039
  • 3
  • 29
  • 50
  • 1
    I assume the `TCNT1` calculation is not _always_ off by one (timing 2?) , just _sometimes_ (timing 1)? – chux - Reinstate Monica Feb 17 '17 at 22:34
  • @chux yes, `TCNT1` is only sometimes off by one. I'm not really sure how to explain it in words but the timing diagram I included shows how this happens. – AJMansfield Feb 17 '17 at 22:38
  • Is the ISR routine fast enough to sample and certainly detect a `clk_Tn` pulse? Can `clk_Tn` set a flag that is clearable by the ISR? – chux - Reinstate Monica Feb 17 '17 at 22:40
  • @chux hmm, that might work. There's no way I could do it with the timer 1 registers; I'm already using both compare registers for PWM, but timer 0 shares the same prescaler module so it could be used. The issue with this approach is that timer 0 is already used by a bunch of library code that I'd have to reimplement. – AJMansfield Feb 17 '17 at 22:45
  • As I see it you need 1) ISR routine only called at a time away from `clk_Tn` or 2) Detect `clk_Tn` occurred in ISR. Anything else will only minimized the problem, not eliminate it. Perhaps posting the "abusing the PWM output to trigger" may provide another attack avenue. – chux - Reinstate Monica Feb 17 '17 at 23:51
  • ICR1 and TCNT1 supposed to be the same when read in an input capture ISR. Is that correct? – Koorosh Hajiani Feb 18 '17 at 00:41
  • Can you stop the timer, then update TCNT1, and finally start the timer again after an integer number of prescaler ticks? – Terje D. Feb 18 '17 at 18:35
  • @KooroshHajiani input capture stores the current TCNT1 value to ICR1 immediately on receiving the ICP1 signal, even if the TIMER1_CAPT interrupt can't be run immediately. – AJMansfield Feb 20 '17 at 16:52
  • @TerjeD. the issue being that I'd need to make sure that starting and stopping the timer are an integer number prescaler of ticks apart, which is back to the original problem of ensuring the timer read and write are an integer number of prescaler ticks apart. – AJMansfield Feb 20 '17 at 16:54
  • With a prescaler setting of /8, you will then need the read and write to happen 8, 16, or 24 clock cycles apart, which should be easy to obtain by checking the disassembly of the interrupt routine, inserting some dummy instructions as needed, maybe coding part of the interrupt routine in assembly. – Terje D. Feb 20 '17 at 21:36
  • I usually find a way to do it where I don't have to edit a running timer. I often configure a timer to count up until it reaches a pre-set comparison value and then it resets itself as it triggers an interrupt. You would edit the comparison value right after it resets itself, but leave the running counter alone. If you say more about what you are trying to do and what AVR you using you are more likely to get a solution. – David Grayson Feb 23 '17 at 04:29

1 Answers1

1

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.

Terje D.
  • 6,250
  • 1
  • 22
  • 30