Electronic – Pic16 Timer0 puzzle

cinterruptsmicrochippic16ftimer

INTRO

The whole point of a timer with overflow interrupt is that the interrupt will trigger on a precisely timed interval – so long as the code executed in the interrupt doesn't take longer than the timer interval.

THE PROBLEM

On a PIC16LF1823, I have run into the following behavior and I don't understand it. In short, one of my routines was being called from within the interrupt and was causing the interrupt to delay firing the next time. Its as though the routine somehow causes Timer0 to pause while its running. I've reduced the problem code as much as I can.

STARTING WITH WHAT WORKS

First, the basic code with no frills sets the processor to 2 MHz, starts Timer0 with no pre-scaler, and in the interrupt routine the test pin (C3) is toggled to verify everything is working. (Incidentally, I'm using mikroC). This code works as expected:

void main() {
    //2MHz
    SPLLEN_bit = 0;
    SCS1_bit = 1;
    SCS0_bit = 0;
    IRCF3_bit = 1;
    IRCF2_bit = 1;
    IRCF1_bit = 0;
    IRCF0_bit = 1;

    //Oscilloscope probe on C3
    ANSC3_bit = 0;
    TRISC3_bit = 0;
    LATC3_bit = 1;

    //Start Timer0
    TMR0CS_bit = 0; //Internal clock
    PSA_bit = 1;    //No prescaler assigned to Timer0

    //Enable timer interrups
    TMR0IE_bit = 1;

    //Enable general interupt

    PEIE_bit = 0;
    GIE_bit = 1;
    LATC0_bit = ~LATC0_bit;
}


void interrupt(void){

      if (TMR0IF_bit) {
         //The interrupt fires every 132uS
          TMR0 = 255 - 121;
          TMR0IF_bit = 0;

          LATC3_bit = ~LATC3_bit;
          //Delay_us(100); //<<<<Uncommenting this widens the pulse, but the pulses are still 132uS apart
          LATC3_bit = ~LATC3_bit;
      }
}

I have verified this works. Pin C3 toggles once every 132 µs with or without the delay.

HOW TO BREAK IT

However, in the following code, the interrupt now fires every 136 µs, instead of 132 µs. The only difference is the addition of two function (routine1 and routine2), and the declaration of an unsigned short. As I'm asking this question, just removing the initialized value ('=0') fixes the problem.

void main() {
    //2MHz
    SPLLEN_bit = 0;
    SCS1_bit = 1;
    SCS0_bit = 0;
    IRCF3_bit = 1;
    IRCF2_bit = 1;
    IRCF1_bit = 0;
    IRCF0_bit = 1;

    //Oscilloscope probe on C3
    ANSC3_bit = 0;
    TRISC3_bit = 0;
    LATC3_bit = 1;

    //Start Timer0
    TMR0CS_bit = 0; //Internal clock
    PSA_bit = 1;    //No prescaler assigned to Timer0

    //Enable timer interrups
    TMR0IE_bit = 1;

    //Enable general interupt

    PEIE_bit = 0;
    GIE_bit = 1;
    LATC0_bit = ~LATC0_bit;
}


unsigned short mode=0; //<<<<<<<<<<Removing the initiator ('=0') fixes the problem!!!
void routine1(void){
    unsigned short result;

    mode = 0;
    switch(mode) {
    case 99:
        //result = someFunction();
        if (result == 0xFF) {
            //Do something
        }
        break;
    }
}

void routine2(void){
    unsigned int result;

    if (result == 0xFF) {
       //Do something
    }
}

void interrupt(void){
int tmp;

      if (TMR0IF_bit) {
         //The interrupt fires every 132uS
          TMR0 = 255 - 121;
          TMR0IF_bit = 0;


          LATC3_bit = ~LATC3_bit;
          //Delay_us(100);
          LATC3_bit = ~LATC3_bit;


          routine2();
      }
}

Can anyone explain this?


EDIT1:

CORRECTION, the clock is set to 4MHz, so the clock is running at 1uS (1 / instruction clock). The code comment above is wrong.

Bruce's and m.Alin's comment about the magic number got me to thinking. I should be able to hard code 132. And then it occurred to me in the original code:

void interrupt(void){

    if (TMR0IF_bit) {
        TMR0 = 255 - 121; //Produces 132uS, but proves unpredictable       
        TMR0IF_bit = 0;

        LATC3_bit = ~LATC3_bit;
        LATC3_bit = ~LATC3_bit;
    }
}

the line:

TMR0 = 255 - 121;

is an attempt to get the timing right, having taken into account anything that happens after the interrupt fires but before I get a chance to reset the timer- thus the magic fudge factor of 11. In other words, I'm trying to predict what the value of the timer is at the time I preset it.

The fact is I know the value- its held in TMR0. So I tried the following line next:

TMR0 = TMR0 - 132; //Predictable but off by 3uS

This produced 135uS pulses, because of the if statement, and the subtraction. So, I subtracted 3 from 132:

TMR0 = TMR0 - 132 - 3; //This produces 142uS pulses (132uS + 10uS for the extra operation)

But of course, that didn't quite work because while reducing the time by 3, I also added a couple of operations which took even more time. So I tried the following…

TMR0 = TMR0 - 129; //This produces 132uS pulses (132 - 3)

This seems to have fixed the problem. This must mean the interrupt isn't firing in exactly the same way every time. Something must be holding the interrupt off at times. So even though this is a fix, it leaves me not know exactly how the timer interrupt mechanism works.

Any ideas?

Best Answer

When managing hardware at a low level like this you must:

  1. Read the datasheet.

  2. See point 1.

  3. Be sure you know what machine instructions are being used. The easiest way to do this is to write critical code in assembler.

  4. See point 1 again.

You failed on points 1 and 3. If you had carefully read the section on timer 0, you would have seen that it stops for two cycles after any write to it. This is clearly described on page 175, section 20.1.1 8-Bit Timer Mode, paragraph 2:

When TMR0 is written, the increment is inhibited for two instruction cycles immediately following the write.

As you eventually figured out, you don't want to set TMR0 to a fixed value in the interrupt, but rather add a fixed value to it. Since the add is relative to the current value, it doesn't matter where in the interrupt routine the add is done, so long as the interrupt routine doesn't exceed the desired timer period.

When doing the add, the three cycle loss of increment needs to be taken into account. Put another way, when adding to the timer each period, the base period becomes 259 cycles, not the 256 cycles when the timer is left alone. The value added into the timer is how many cycles the period is shortened by from 259. If you want 132 cycle period, then you add 256 + 3 - 132 = 127 to TMR0 each period.

This, of course, needs to be done in assembler to guarantee the timer is incrementally written to the right way. You have no guarantee what instruction exactly the C code

TMR0 = TMR0 + (256 + 3 - period);

will generate. For example it could make a mess:

         movf    tmr0, w
         addlw   256 + 3 - period
         movwf   tmr0

You want to write this yourself:

         movlw   256 + 3 - period
         addwf   tmr0

Even better is to wrap all this into a macro. That better documents the intent in your interrupt code and make it something you only have to figure out once carefully, then use as a canned resource on new projects. I did this long ago. Here is my macro:

;********************
;
;   Macro TIMER0_PER cy
;
;   Update timer 0 so that it next wraps CY cycles from the previous wrap.  This
;   can be useful in a timer 0 interrupt routine to set the exact number of
;   cycles until the next timer 0 interrupt.  Timer 0 is assumed to be running
;   from the instruction clock.  The appropriate value is added into timer 0,
;   so this macro does not need to be invoked a fixed delay after the last
;   timer 0 wrap.  CY must be a constant.
;
;   The timer sets its interrupt flag when counting from 255, which wraps back
;   to 0.  If left alone, the timer therefore has a period of 256 instruction
;   cycles.  When adding a value into the timer, the increment is lost during
;   the add instruction, and the timer is not incremented for two additional
;   cycles when the TMR0 register is written to.  This effectively adds 3 more
;   cycles to the timer 0 wrap period.  These additional cycles are taken
;   into account in computing the value to add to TMR0.
;
timer0_per macro cy
         dbankif tmr0
         movlw   256 + 3 - (cy)
         addwf   tmr0
         endm

This is one of the many utility macros in STD.INS.ASPIC in my PIC Development Environment.