6
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ Where exactly do you mean by moving it to the beginning of the block? Between which two lines of code? \$\endgroup\$ – AlmostDone Feb 6 '18 at 21:10
  • \$\begingroup\$ @AlmostDone Right after the if (TMR0IF) { (where { marks the beginning of a block in C syntax). \$\endgroup\$ – Havenard Feb 6 '18 at 21:42
  • 3
    \$\begingroup\$ You are doing divides in the ISR! Go learn the machine before writing any more high level language code. \$\endgroup\$ – Olin Lathrop Feb 6 '18 at 23:19
13
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Yeah I know exactly what you mean, also the time it takes just to figure out what byte it should print is long enough that is causing the LEDs to blink, something that I wasn't having in my Arduino prototype. I tried to optimize the thing with some bitwise operations but apparently the compiler already does that when applicable, I'm not sure how to optimize the %10 and divisions by powers of 10 though. I'm trying to look into some binary to BCD conversion codes but so far all the ones I've found don't look optimized at all. \$\endgroup\$ – Havenard Feb 7 '18 at 0:28
  • \$\begingroup\$ So far my best solution was to apply a pre-calculated PORTB and then pre-calculate the PORTB for the next cycle, so that there is no delay to light the LEDs. And also use 20MHz crystal, but I'd rather find a solution for the 4MHz one. \$\endgroup\$ – Havenard Feb 7 '18 at 0:30
  • \$\begingroup\$ Right on the money Olin. It makes me glad I learned from doing hexadecimal then assembler first, before learning C then LabView. OP needs to learn the basics of how code runs in the kernel. \$\endgroup\$ – Sparky256 Feb 7 '18 at 7:02
5
\$\begingroup\$

I don't think you should ever put TMR0IF=0 (clear the interrupt flag) at the beginning of your ISR.

I think what is happening is your ISR routine is taking more than 800 us or your TMR0 is triggering faster than you think. By putting TMR0IF=0 in front, you are clearing the interrupt and allowing for another TMR0 interrupt to interrupt the interrupt. The MCU pushes the present info into the stack, and calls the same interrupt again. Eventually your stack will overflow and your system will crash.

  1. Keep TMR0IF=0 as the last instruction
  2. Time your ISR to see if it is shorter than 800 us.
  3. Verify if TMR0 is running at the rate you think.

I time my routines by driving a GPIO high at the beginning and driving same GPIO low before exiting the routine. Use a o'scope on that GPIO and you can verify a) if your ISR is really less than 800 us, b) if your TMR0 is really calling at the rate you think.

The pulse width will tell you how long your ISR is taking. The period will tell you how often your ISR is being called. If the period of the o'scope signal is erratic, then your ISR is taking longer than TMR0 timer periods (i.e. is skipping TMR0 periods)

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Good overview, I think. +1. I have, however, cleared the interrupt flag well before exiting interrupts in cases where I am at a point where I can permit (and may want to permit) re-entry prior to completion of an earlier interrupt. It's not often I feel a need to do that, though, and it is usually because I didn't want to set up a flag that is recognized elsewhere for "more work" and instead wanted to just cheat and hook up the extra work that occurs once in a while right in the interrupt code. Not often, as I say. \$\endgroup\$ – jonk Feb 6 '18 at 21:47
  • 1
    \$\begingroup\$ I doubt PIC16F supports such thing as a call stack for ISR, there's a reason it only has 1 ISR. \$\endgroup\$ – Havenard Feb 6 '18 at 22:07
  • 1
    \$\begingroup\$ Yeah to my surprise it turns out the ISR procedure is taking longer than the duty cycle, for some reason, so the PIC is always busy processing interrupts and never resume the main program. This is crazy, the ISR does almost nothing. Unless... the PIC doesn't support modulo or division and the compiler is adding code to perform those tasks? In any case if I use 20MHz crystal it seems to workaround the issue. \$\endgroup\$ – Havenard Feb 6 '18 at 22:50
  • 1
    \$\begingroup\$ -1 On PIC16Fxxx, the ISR can't be interrupted be another ISR. \$\endgroup\$ – m.Alin Feb 7 '18 at 10:49
  • 1
    \$\begingroup\$ Also, take a look at the assembler output of your c code. You can get an idea of how that big that integer math function (calculating the font index) expands out. Integer math is fast but if you're using 32-bit integers, or even 16-bit integers that's going to turn into a whole bunch of 8-bit instructions! \$\endgroup\$ – Vince Patron Feb 7 '18 at 19:35
1
\$\begingroup\$

The answer is already address in the other post and comment (a "LOT" of div, and the ISR taking to much time to do the stuff), but I would point out that it comes out as rude.

Are we still in the stone Age(kidding)? Most of the example on internet gives you c example, why? because it is simpler to understand and can be changed more freely then an assembler code. Sure, the code written in assembler is faster (maybe from someone who has spent a lot of time coding with it), but for a simple task the compiler do a fine job too.

The problem IMHO, it is the source of programming skill of the op, if he is used to develop software, you don't realize how many resource the program is using, because you are working with a higher performance hardware, and not with a limited pic16.

Div and mod, are sure taking a lot of time to be executed (on pic), but if you optimize the process, maybe in some case it is viable. For example:

i % 8U -> i&7
i / 8U -> i>>3

Some div and mod, can be simplify using shift or logic operation (in some case where the number are mod 2), but you learn tricks by trying things and sometime making mistakes.

With the load of work on the isr, it is better as suggested, to set a flag, and in the main update your data, but trying to improve your code, helps you in future project.

I don't want to be harsh, but everything should be evaluated for the final scope. In this case, addressing the workload of the pic should be the right way to point out the problem, rewriting in assembler what achieve? Adding more complexity doesn't solve it, saying for example: your oven(micro) can cook 1 pizza(n instruction) every 10 min(before the ISR is called again), but you are asking to cook 10 pizza at the same time, gives probably a concrete idea of the problem.

Just be aware of what you are using.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I partially agree, also all those optimizations you suggest, any C compiler that is not total garbage should do by default. Machine code generated by the compiler is as fast, and often faster than code you try to write on your own. However, writing in Assembly makes sense in some projects, because it allows you a very tight memory management, and when you are programming microcontrollers, memory can be a luxury you don't have. \$\endgroup\$ – Havenard Feb 8 '18 at 23:17
  • \$\begingroup\$ In this project I'm using roughly 10% of memory available, which is a lot considering how little variables I have. If I was to do the same project on a PIC12C508, I'd have run out of memory. In Assembly this wouldn't be a problem. \$\endgroup\$ – Havenard Feb 8 '18 at 23:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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