Electronic – Why the place where I clear TMR0IF changes the behavior of the program

microcontrollerpic

I'm programming a PIC16F877A microcontroller in C, compiling with Microchip XC8 1.44.

I have set up a TMR0 interrupt for every 800 microseconds that scans through a LED matrix, lighting each 8 LED column at a time using the settings as follows:

    OPTION_REG = 0x04; // TMR0 prescaler (1:32)
    GIE    = 1;
    TMR0IE = 1;

And this is my ISR:

void interrupt isr()
{
    static const uint16_t divs[4] = { 1000, 100, 10, 1 };
    if (TMR0IF)
    {
        PORTB = 0x00; // PORTB is the 8 LED column
        if (i)
        {
            PORTAbits.RA0 = 1; // pin that clocks CD4017 for next column
            __nop(); // give a brief delay just for good measure
            PORTAbits.RA0 = 0;
        }
        else
        {
            PORTAbits.RA1 = 1; // pin that resets CD4017 to first column
            __nop();
            PORTAbits.RA1 = 0;
        }

        // pattern to be displayed
        PORTB = font[i % 8U + 8U * ((c / divs[i / 8U]) % 10U)];
        i = (i + 1U) % 31U; // increment i for next iteration

        TMR0 = TMR0_OFFSET; // 240 for 4MHz crystal for 800us delay according to my calculations (I am bad at calculations)
        TMR0IF = 0;
    }
}

Basically c (for 'counter') will hold a number between 0 and 9999, and the 31 column LED matrix displays it using the data in font to draw the numbers.

Now, this code works just right. But if I move TMR0IF = 0; to the beginning of the block, something strange happens:

The program seems to become slow, as if the clock dropped by a factor of 100 or more, and the main program loop crashes and because of that I stop receiving the external signals that I use to increment or reset the value of c, only the ISR remains being called and keep the LED matrix lit, but flickering due to the clock drop.

If I move TMR0IF = 0; to the end like in the code above though, everything seems to work normally and the world is perfect. But why?

Everywhere I see code reference for Timer 0 interrupt usage people set TMR0IF = 0; right away, why is it causing all that hassle in my code?

Best Answer

The basic problem is that your interrupt code is taking longer to execute than the time between interrupts. This probably due to one of two problems:

  1. The interrupt period isn't what you think it is. The interrupt condition is actually being triggered more often than intended.

  2. You are taking too many cycles in the interrupt routine.

The reason it appears to work when you clear the interrupt condition at the end of the ISR is that there is then some time for the foreground code to run before the next interrupt. However, that is NOT a solution to the problem. When you move the clearing of the interrupt condition to the start of the ISR where it should be, the ISR takes so long that the next interrupt is taken immediately when the ISR ends. No, this does not overflow the stack as another answer claims. However, it does lock out the foreground code from getting cycles.

Always clear the interrupt condition right at the beginning of the interrupt routine. If using timer 0 for the interrupt period DO NOT set TMR0 to a fixed value each interrupt. Instead, add the appropriate quantity each interrupt. That way you don't lose time based on interrupt jitter latency.

Also, don't use the prescaler when adding a offset into the timer. If using timer 0, either set add a offset each interrupt to get a specific period with the prescaler 1, or use only the resulting free-running period with non-unity prescaler.

If you want more arbitrary interrupt periods, use timer 2. That's what it's for.

I notice that you wrote your interrupt routine in C. I've seen some pretty horrible code bloat due to the C compiler not knowing what really needed to be saved, and therefore saving lots of stuff. Personally, I write PIC 16 interrupt routines in assembler. It also makes it easier to figure out what is going on in cases like this, because there is no compiler between you and the hardware.

I just scanned your ISR code. You are doing divides in the ISR! No wonder it's taking longer than intended.

This is a great example of why on small microcontrollers like this, compilers should never be used as a substitute for understanding the machine at the instruction level. The compiler must only be a shortcut for creating a bunch of code for you, but you still need to have a basic understanding of what that code is and what the machine has to do to implement what you ask.

Put another way, you have to architect the code while thinking of the machine's raw capabilities. Once you do that, using a compiler to generate some of the actual code for you can be a legitimate shortcut. Even then, the ISR is the one routine you want to most consider writing in assembler.

Go write this interrupt routine in assembler. That's not because it necessarily needs to be in assembler, but because you need to learn the machine. You have no business being here without understanding the instruction set and the other low level capabilities of the hardware. You never would have tried to do division in a ISR if you really understood the instruction set.