The answer is, as little as possible that cannot be done logically in the background code. To make your code run quickly and smoothly interrupts are necessary, but they should only do what absolutely has to be done by the Interrupt Service Routine. This is mainly due to the fact that while in the ISR, interrupts are disabled and an interrupt cannot be called again until the program has exited the ISR. Interrupts should not call functions or use delays. Interrupts should also only use local variables if they are needed, they should never modify global variables within the ISR itself.
The subject of interrupts is very extensive. Even on our elementary approach to PIC microcontroller programming we are taking two lessons to cover them, and we won't really even scratch the surface. The reason behind this is mostly that there are many interrupt sources, and an endless list of reasons to use interrupts. The true purpose of this text is to give you a basic understanding of programming PIC microcontrollers in C, and to give you a base of knowledge that allows you to write working programs. To that end, my goal with these lessons on interrupts is to give you the basic principals of configuring and using them, not to make you an expert. Once you have the basic knowledge, it will be easier for you to absorb a more thorough text from external sources.
This lesson will talk about getting into and out of interrupts quickly, clearing interrupt flags, creating our own flags, and polling interrupt sources.
THE INTERRUPT SERVICE ROUTINE
For Mid-Range PIC micros, there is only one interrupt vector which, as we talked about in the previous lesson is at address 0004h. When any interrupt occurs in our source code, the program will always point to this vector.
In C we define the interrupt function using the function qualifier "interrupt". An interrupt function must be declared as type void and cannot contain parameters. We must also assign a name to the function as we do any other function. A typical interrupt function name might be "isr". With these rules in mind, an interrupt function declaration would typically look like: void interrupt isr(void)
When writing programs with interrupts we enable the "interrupt enable" bits for the interrupt sources we want to allow. We also enable the "global interrupt enable" (GIE) bit which turns on interrupts. When our program is running and an interrupt request is received, the Interrupt Flag bit of the corresponding interrupt source is set. This interrupt flag bit verifies the source of the interrupt. You should know that interrupt flags will get set when the right conditions exist regardless of the state of the interrupt enable bit. The interrupt enable bit is used only to determine whether or not an interrupt flag going high will actually trigger an interrupt. When entering into the ISR, the programmer should test the status of the interrupt enable bit and the interrupt flag bit before executing any instructions. This is called "polling", and is a safeguard to prevent unwanted code from being executed. It is also the method used to separate what code gets executed when our interrupt function responds to multiple interrupt sources. More on this in a moment.
Consider the following scenario: You are sitting in your chair reading a magazine when you think you hear someone call your name. You stop reading for a moment and listen but don't hear anything else, so you determine that you must have been hearing things and you return to your reading. Conversely, if someone had in fact called your name you would answer them and respond to their request before returning to your reading. This is similar to polling the interrupt flag bit.
When program execution jumps to the interrupt vector, the Global Interrupt Enable (GIE) bit is cleared automatically, disabling further interrupts while in the ISR. At the end of the ISR the GIE bit is reset. This is done automatically by the compiler by placing a RETFIE instruction at the end of the ISR. RETFIE is an Assembly language instruction which says "Return from interrupt with Interrupts Enabled". Additionally when we enter the ISR, the contents of the Working and STATUS registers are saved so they can be restored at the end of the ISR. This is done because interrupts can occur at any time, and will likely modify the values in these registers. The instruction that gets executed first after exiting the ISR may depend on the values that were in these registers before the ISR occurred. When exiting the interrupt, these registers are restored to their previous values.
WRITING AN INTERRUPT SERVICE ROUTINE
We will set up a new project here to demonstrate using an interrupt. We will use two LEDs connected to a PIC12F629 microcontroller. LED 1 is a red LED connected to GP4 and LED 2 is a blue LED connected to GP0. The LEDs are connected to ground through 220Ohm current limiting resistors. Additionally, we will connect a Normally Open momentary push button to the GP2/INT pin, which will be our external interrupt source.
Upon applying power, the microcontroller will turn on the blue LED and wait for a button press. The blue LED shows us that things are on and working. A push of the button initiates an interrupt which turns off the blue LED and flashes the red LED on/off 20 times to simulate a "busy" indicator. So that we can see these flashes, we will include 50 millisecond delays for each on and off flash. Figure 1 shows our project schematic.
Figure 1. |
Figure 2. |
The GPIO register is initialized as clear, and the TRISIO register is setup for the button input on GP2.
Our LED masks are defined for GP4 and GP0.
TRUE and FALSE bit variations are defined for easier reading of the code.
The Oscillator frequency is defined.
We then define our LED on and off functions. I have used an alternative method for turning off the LEDs here to demonstrate different ways to accomplish the same task. Rather than our usual exclusive OR (XOR) bitwise operator, we are now bitwise ANDing GPIOimg with the inverse of the masks. The inverse of the mask is to swap the state of the bits turning ones into zeroes, and zeroes into ones. The blue LED mask is 00000001, so the inverse would be 11111110. ANDing GPIOimg with this inverse results in:
GPIOimg: 00000001
~mask: 11111110
result: 00000000
The result is then copied as usual to the GPIO register turning off the LED.
After our I/O functions is our section for the Interrupt beginning on line 64. Our interrupt function is declared using void interrupt isr(void).
Like all other functions, the interrupt instructions are contained in curly brackets. Line 67 shows where we poll the status of the interrupt specific enable and flag bits. We test both bits in the same line by using the logical AND operator, which says if the enable bit AND the flag bit are true the result is true. Don't confuse the bitwise AND (&) with the logical AND (&&). If the result here is true, then the instructions within the curly brackets get executed. If not, they get skipped.
Within the interrupt function, we only perform two instructions. First we set our own "request" flag bit (line 69), and then we clear the Interrupt Flag bit (line 70). If we don't clear the interrupt flag bit, we will never exit the ISR and will be stuck in an endless loop there. It is 100% up to the programmer to clear the flag bits. Clearing the flag bit also allows the interrupt to occur again.
Line 73 begins the main program section. TRISIO, GPIOimg, and GPIO are all initialized by copying the initialization values to the appropriate registers.
Our own request flag is cleared at startup on line 79, since we don't want the program instructions associated with this flag to execute until an interrupt actually occurs.
Line 80 clears the INT interrupt flag at startup.
Line 81 enables GP2/INT interrupts.
Line 82 enables global interrupts. Remember that even if we have the INT interrupt enabled, an interrupt will not occur unless global interrupts are also enabled.
Now the program turns on the Blue LED on line 83 before we enter into our endless while(1) loop.
The while(1) loop sits and waits for the request bit to be set. When we press our button, the input signal on GP2 sets the INT interrupt flag bit (INTF) and the program goes to the ISR. In the ISR, the request bit is set and INTF is cleared and program flow returns to the while(1) loop where it left off. Now the request bit will return a true result, and the instructions in the if function will be executed.
Using the bit variable "request" here moves all of these instructions outside the ISR so that we can get into the ISR and back out of it as quickly as possible while still getting the result we want to. In this case, to turn off the blue LED, flash the red LED 20 times with 50 millisecond delays, and turning the blue LED back on.
Those instructions are all contained in the if function. The 20 flash count is achieved by using the local variable "count", and will decrement once each pass until count is no longer greater than zero.
Just like clearing the INT flag bit, we will want to clear the request bit within the if function, otherwise we will never stop executing that function since request will evaluate true every time. Clearing the request bit gets us out of the if function and back to waiting for another button press.
Using interrupts is simple once you understand the process. In later lessons, we'll look at using more than one interrupt in a program.