Tech Tip: My Interrupts Don't Work!

If you have ever uttered the above statement, there are a couple things you should check in your code:

(a)

Does your ISR (or any function called from your ISR) modify any variables that are also accessed in any way (read or write) outside that ISR?

(b)

Does your ISR access in any way (read or write) any variables larger than a single byte (or, any bitfields) that are also accessed in any way outside that ISR?

These two checks, which look similar, will actually tell you if you have tripped over one of two different issues - the first related to the abstract "virtual machine" modeled by the C language, and the second related to the much more concrete machine provided by your chip.

(a)

The C "virtual machine" is single-threaded - the compiler is entitled to assume that, if a variable is not modified within any given section of code, then it will have the same value after that section as it had before. If an interrupt routine modifies a variable that your main program reads in a loop but never modifies, the main program may have been optimized by the compiler to never read that value from RAM after the first time, and (possibly) loop forever or never depending on that initial value. So, with a tiny program like:

unsigned char x;
interrupt void isr(void) {
        if (condition) x++;
}
main(void) {
        while (!x) /* wait for x to become non-zero */ ;
        /* code here */
}

The compiler is allowed to assume that, since x is never changed by code inside main(), that it will never change inside main(), and thus (since x is initialized to zero) main()' is equivalent to:

main(void) {
        while (1) /* wait forever */ ;
        /* code here has all been removed because it can't be reached */
}

(b)

An int is at least 16 bits, and many chips have no instruction which can read 16 bits from, or write 16 bits to, RAM in a single atomic operation. (Those which can will have the same problem with data types such as long int, double, or larger structure or array types, so the same argument applies.) It would be perfectly feasible for your main program to read 1 8-bit byte from an int variable, get interrupted (which modifies the variable in RAM) and then continue, reading the second byte which now bears no valid relationship to the first - the main program would be operating with corrupt data. Similarly, the main program could write the first of two modified bytes to RAM, and get interrupted before writing the second byte to RAM - in this case, the interrupt routine gets corrupt data, for example:

int x;
interrupt void isr(void) {
        if (x == 256) do_something();
}
main(void) {
        while (1) {
                do_something_else();
                x++;
        }
}

If x held the value 255, then the x++ could increment the low byte (at which point the value stored in memory is 0), an interrupt occurs which sees that x is still less than the threshold, and then the main routine gets to carry the increment into the high byte; if the next interrupt does not happen until after the next time x++ begins then the fact that x reached 256 would have been missed.

(This same problem can also occur with bitfields even though they may be small enough for the chip to fetch or store in a single instruction, due to the multiple instructions which may be necessary to separate them from the other bitfields stored within the same byte).


Never Fear! We have the Technology to Solve these Problems.

(a)

Any variable which is modified in an ISR and accessed outside that ISR must be qualified volatile to ensure that the compiler does not optimize away any read of the value. Without this, your main-line code may not actually read the updated values of such variables because the compiler believed you when you promised (and lied) that they would not change behind its back. Thus, our tiny test program becomes:

volatile unsigned char x;
interrupt void isr(void) {
        if (condition) x++;
}
main(void) {
        while (!x) /* wait for x to become non-zero */ ;
        /* code here */
}

And the compiler is required to not assume that x will never change within main() - it must generate a loop that tests the variable on each iteration because the volatile qualifier tells it that the variable may change at any time.

(b)

Any variable larger than a single CPU register (or, any bitfield) which is accessed both from within and outside an ISR should be wrapped by di() and ei() (defined in <htc.h>, or the chip-family header if using one of our older compilers) when accessed outside the ISR, to ensure that an interrupt does not happen while the object is part-way through being accessed. Without this, your main-line code or interrupt routine or both may work with corrupted values when part of such a variable is modified before and part after an interrupt occurs, or when part is read before and part after an interrupt during which the whole is modified. Our example now becomes:

#include <htc.h>

int x;
interrupt void isr(void) {
        if (x == 256) do_something();
}
main(void) {
        while (1) {
                do_something_else();
                di(); /* protect the integrity of 'x' */
                x++;
                ei(); /* OK, 'x' is consistent again */
        }
}

Note: You must put such protection around any access to such a variable outside an ISR, including anywhere that it's involved in the condition of a branch or loop; if testing other than a single variable, or in a loop whose body is very small, you may want to use a non-interrupt-accessed temporary to reduce the complexity.


Tying all of the above together:

#include <htc.h> /* defines 'di()' and 'ei()' macros */

volatile unsigned int a; /* modified inside ISR */
         unsigned int b; /* modified outside ISR but not inside */

volatile unsigned char c; /* modified inside ISR */
         unsigned char d; /* modified outside ISR but not inside */

         unsigned int t; /* temporary, used outside ISR only */

main(void) {
        while(1) {
                di(); /* protect multi-byte 'a' */
                if (a) {
                        ei(); /* done with 'a' */
                        /* do stuff */
                        /* no precautions need be taken here
                           because access to 'd' is atomic */
                        d++;
                        /* do more stuff */
                }
                else ei();
                /* remember to re-enable after a loop or conditional
                   as well as inside it */

                di(); /* protect test */
                while (a) {
                        ei(); /* done with 'a' */
                        /* do inside-loop stuff */
                        di(); /* protect next test */
                }
                ei();
                /* remember to re-enable after a loop or conditional
                   as well as inside it */


                /* stretch the window for the interrupt to do its job
                   when the loop body is very small */
                di();
                t = a;
                ei();
                while (t) {
                        /* do tiny bit of inside-loop stuff */
                        di();
                        t = a;
                        ei();
                }


                /* no precautions need be taken here
                   because access to 'c' is atomic */
                if (c) {
                        /* do before-b stuff */
                        di(); /* protect multi-byte 'b' */
                        b++;
                        ei(); /* done with 'b' */
                        /* do after-b stuff */
                }
        }
}

interrupt void ISR(void) {
        /* no need for further protection here
           as this ISR can't be interrupted;
           a 'low_priority' ISR would need it, though, if it accessed
           a multi-byte quantity also accessed here */
        a = b * 23 + 1;
        c = d + 47;
}

 

June 2006.