A Tutorial on PIC interrupts using BoostC including Example Programs
Contents
A Tutorial on PIC interrupts using BoostC including Example Programs[edit]
This is still a draft, but mostly right
A nice feature of many ( probably all that you want to use ) PICs is the interrupt feature. This makes some programming tasks much easier, and may make the impossible possible. This article will give some tips on the why and how of using interrupts. Please let me know if you find areas needing improvement, or just make them yourself. The code here is extracted from real working projects. Normally it has been somewhat simplified, and some of the declarations and setup are not shown. In all cases there are links to the actual project where all the code is, and, as far as we know works.
What is an interrupt?[edit]
An interrupt is a function in a microcontroller where some event ( change of an input, count or time measured from a timer, character received from a UART ) causes the microcontroller to stop what it is doing and go to a special subroutine designed to “service” the interrupt. It then goes back to where it was “interrupted” and “goes on like nothing ever happened”. It is very useful for tasks that are time critical, that is where something has to happen fast or at an exact time.
Example Time keeping for a Clock[edit]
PIC based Stepper Motor Dancing Analog Clock to link to the whole project.
Obviously time keeping is an important task for a clock. 99% accuracy is not very good, it goes off time by about 10 minutes a day.
The problem:[edit]
In this solution we will use the power line as a source of time keeping. The power company normally does a very good job keeping the long term average very accurate. In the power supply we clip the 60 Hz ac to an approximate square wave and feed it into the RB0 external interrupt. The key is to make sure that every transition from low to high is counted toward the time. Since the clock also has to move the clock hands we could have a problem if the microcontroller is busy moving the hands and “forgets” to count.
Solution:[edit]
The RB0 input can generate an interrupt if it is set up and enabled. This will jump the control to the subroutine named interrupt() where we will count the 60 Hz signal.
Features of the external interrupt that we will use.
The interrupt is triggered by an input on RB0, either ( depending on how we set it up ) as the signal goes from 0 to 1, called the rising edge, or as the signal goes from 1 to 0, called the falling edge. This sets the interrupt flag, and if the interrupt is enabled ( and we are not already in an interrupt ) the microcontroller goes to the interrupt subroutine.
The program design:[edit]
All the timing will take place in the interrupt service routine ( isr ). A flag ( global variable ) will be set any time the minute changes and individual global variables will hold the time in seconds, minutes...
With most interrupt program there is the setup, the interrupt itself, and post interrupt processing. Here we go.
Setup:[edit]
Setup occurs in the main-line code, before the main loop.
The RBO interrupt -- Clear the flag, set for rising edge, and enable the interrupt.
int main(void){ ....
clear_bit( intcon, INTF ); set_bit( option_reg, INTEDG ); // set for rising edge set_bit( intcon, INTE ); // set to enable the interrupt
....
// main loop while(1){ // forever .... }; }
Here we assume the interrupt is originally off ( or the global interrupt is off ). If you are not sure then turn them off at the beginning.
The interrupt:[edit]
In interrupts we almost always make sure that the interrupt is the one we think it is, clear the flag, and do the processing. A key to the post processing is setting the counts and the MinAct flag.
The interrupt routine handles stuff we absolutely must deal with immediately during the interrupt (the "foreground task").
if ( intf ) { // are we in the external interrupt. clear_bit( intcon, INTF ); // set on interrupt, need to reset SubSecCount ++; if ( SubSecCount >= 60 ) { SubSecCount = 0; SecCount ++; SecAct = 1; if ( SecCount >= 60 ) { SecCount = 0; MinCount ++; MinAct = 1; if ( MinCount >= 60 ) { MinCount = 0; HrCount ++; HrCount24 ++; if ( HrCount >= 13 ) { HrCount = 1; } } } } }
Post interrupt:[edit]
When you get around to it ( but at least every minute ) check the MinAct flag, then do what you have to. We assume that this process takes a bit of time, and you always try to take as little time in the interrupt as possible ( this usually makes the interrupts work better and lets you use more than one interrupt at a time). In fact if you do not get around to executing your code within a minute it may not matter much, the time will still be kept correctly and the flag MinAct will still be set, you will miss executing "what you have to do" once but that may not matter.
Post-interrupt processing occurs in the main loop (the "background task").
int main(void){ ...
// main loop while(1){ // forever ....
if ( MinAct == 1 ) { MinAct = 0; // clear the flag ........ what you have to do ..... };
....
}; }
In the full code you also have to have code to move the hands of the clock, set it, etc. It is in there: PIC based Stepper Motor Dancing Analog Clock
Note: not show here ( but present in the full code ) is the use of the global interrupt enable. It is one instruction that can turn off all interrupts. It has to be turned on before any interrupts will take place. Also in the full code an interrupt is used to support the RS232 receiver.
Example Interrupt Driven Infra Red ( IR ) Receiver[edit]
Experimenting with IR Remotes using a PIC running BoostC Project to link to the whole project.
The problem:[edit]
An IR transmitter sends bursts of infra red to a receiver. This receiver converts the IR to pulses which ( in the case we will consider the NEC protocol ) consists of about 70 transition over a period of time of about .1 seconds. We want to measure these transitions and determine what button was pressed on the IR transmitter. Note that the code here is a bit simplified, the better to show the essential ideas. To make it work go to the project and get the full code. Email me if you are having problems.
Solution:[edit]
The receive goes through several states all of which are interrupt driven.
First we need to wait for a quiet time in the IR signal. To do this we set two interrupts one on timer1 and one on the IR signal. If the IR signal is quiet then the timer1 interrupt will go off first and we are ready to receive, if the timer goes off first then the IR signal is not quiet. We track all of this with a state variable IRState = IRSTATE_WAIT while we are waiting, and IRState = IRSTATE_BUSY IRState = IRSTATE_READY if the IR was quiet and we are now ready to receive.
Issues for IR receive[edit]
- Issue: How do we avoid tying up the CPU during the receive?
Response 1: It only takes about 0.1 sec, so what is the problem? A tenth of a second can be a long time on a PIC and actually if you are waiting to receive something this can take the CPU forever (until the transmitter decides to send something).
Response 2: Use interrupts that occur when the state of the IR signal changes, process quickly and go back to other less time-critical tasks.
- Issue: How do we know when to start receiving?
Response 1: If the IR signal has been quiet for a period of time over about 0.1 sec then the next transition must be the beginning of the signal.
Response 2: If the signal has error checking built in: start receiving wherever and check if the data is good. If it is then you must have started at the beginning. In any case you are now at the end and start receiving the next burst at the beginning.
- Issue: How do we know when a received message is finished?
Response 1: Count the number of transitions in the received signal; done when we get up to the required count. But if we miss a transition we could be stuck.
Response 2: Wait until a quiet period indicates the end of the signal.
Response 3: Some protocols have a special “pulse” or something that we can detect at the end.
- Issue: How do we know the data is good?
Response 1: It usually is, hope for the best.
Response 2: Many protocols have a fixed number of transition, so count and compare.
Response 3: Some protocols have error checking built in. For the NEC protocol the data in the third byte is repeated, and inverted in the fourth byte. If you exclusive-or (XOR) the bytes together, you should get FF or 11111111.
The program design[edit]
We could have the program go in a tight loop counting the number of times it loops and checking the input port for the transition in the IR signal. Each time it changes we record the count and restart at 0. This is how the program IR.c works. It is called a blocking routine because it blocks the microcontroller from doing any other operation (except for interrupts) during the receive. So we will make our routine interrupt driven to end this blocking.
In some interrupt driven programming you can do almost all the work required in the interrupt (blinking an LED for example). But many like this one requires that the work be distributed between the interrupt and non-interrupt processing. And since we jump in and out of the interrupt (in the interrupt for as short a time as we can), we cannot keep track of what we are doing by where we are in the program. Instead we will introduce global state variables - 'global' because any routine anywhere can access the variables, and 'state' because they tell the state of the processing. One of the most important of these is called IRState. The states that are defined for this are:
- Waiting for the beginning of the receive, perhaps because a signal began before we tried to receive. In the program this value is the #define IRSTATE_WAIT.
- Busy meaning that we were waiting for a quiet period, but found that the signal was present. We could continue to wait, but that might put us in a tight loop of waiting, thus keeping the microcontroller busy. We enter this state so that the program knows that we could not begin the receive, and to try again when we get around to it. In the program this value is the #define IRSTATE_BUSY.
- Ready, meaning that we waited for no signal, found it, and are now ready to receive. In the program this value is the #define IRSTATE_BUSY.
- Reading, meaning that we are in the process of receiving. The data should be complete in about a tenth of a second. In the program this value is the #define IRSTATE_READ.
- Got, meaning that we have completed the reading of the signal. In the program this value is the #define IRSTATE_GOT.
- Not applicable is a special state that means that we are not trying to receive anything.
So in a normal receive we go through the states in the order Wait -> Ready -> Reading -> Got and then around again.
OK, so how do we do the programming? I am going to take a bit of an odd approach to the explanation here by focusing on the main part of the receive loop. This is where most of the action occurs, so I think that is a good place to start. Later, I will have to explain how we get into and out of this loop. The plan here is to set up an interrupt on the port for the IR receive (this is port RB0), while at the same time a timer is running. When the interrupt occurs, we will measure the time by reading the timer.
Features of the external interrupt that we will use:
The interrupt is triggered by an input on RB0, either (depending on how we set it up) as the signal goes from 0 to 1, called the rising edge, or as the signal goes from 1 to 0, called the falling edge. This sets the interrupt flag, and if the interrupt is enabled (and we are not already in an interrupt) the microcontroller goes to the interrupt subroutine. Since we want to time all transitions, we will sometimes set the interrupt for the falling edge, sometimes for the rising edge.
Features of the counter/timer we will use:
The timer can be connected to a crystal clock to count up. It is a two-byte counter. We will use only the high byte because we are timing fairly long values. The timer can be turned off or on. We can also divide (or pre-scale) the clock, before counting it. This is useful because it allows us to count longer times, but with less precision.
A feature of the C compiler is that all register values of the controller, which it needs to return to the place where the interrupt occurs, are automatically saved at the beginning of the interrupt (called the “context save”) and restored at the end. In assembler, you need to write the code yourself to do this.
Main Loop[edit]
With most interrupt program there is the setup, the interrupt itself, and post interrupt processing. Here we go.
Setup:[edit]
Since we are looking at the main loop of the reading we are assuming that the interrupt is already set up, we are in the IRSTATE_READ, and will set up for the next interrupt inside the processing of this one at the end.
Interrupt:[edit]
The first thing to remember is that there are several reasons that the program can end up in the interrupt routine. That is because there are different reasons that interrupts can occur. So the first thing to check is that you are in for the reason you think. In the case of the RB0 external interrupt, that is usually done by checking the interrupt flag. I have discovered however that the flag may be set even if the interrupt is not enabled and another interrupt has been triggered. Thus you should consider checking that not only is the flag set but also that the interrupt is enabled. If this is true, we next stop the timer (TIMER0) and read it. Then we reset it for the next count. To get ready for the next interrupt, we need to reset the interrupt flag (set by the processor when the interrupt is triggered), set the edge we are triggering on to rising if it was falling and vise versa.
Post Interrupt:[edit]
Again since we are in the middle of the processing there is no code for this.
The code in the interrupt subroutine, also know as the interrupt service routine or isr:
if ( ( test_bit( intcon, INTE ) ) && ( test_bit( intcon, INTF ) ) ) { clear_bit( intcon, INTF ); // the flag unsigned char period = 0; // time from last transition clear_bit( intcon, INTF ); // the flag // read the timer1 clear_bit( t1con, TMR1ON ); // T1CON: Timer1 On 1 = Enables Timer1 0 = Stops Timer1 period = tmr1h; // just use the high byte // reset timer1 tmr1l = 0; tmr1h = 0; set_bit( t1con, TMR1ON ); // Timer1 On 1 = Enables Timer1 0 = Stops Timer1 // store in the log logData[ logIx ] = period; // set up for next interrupt if ( iRLow ) { set_bit( option_reg, INTEDG ); // set rising edge clear falling edge } else { clear_bit( option_reg, INTEDG ); // set rising edge clear falling edge } iRLow = !iRLow; set_bit( intcon, INTE ); // set = enable } return; }
Note that:
- we save the period in a ( global ) array, logData[ logIx ], for later use,
- set the interrupt edge to the opposite it was,
- and keep a ( global ) variable, iRLow, to tell us if the next state will be high or low ( iRLow )
OK that is the heart of the routine, how do we get in and how do we get out?
Getting In:[edit]
Getting In: For this code ( and I am not sure this is a great way, but it works and uses more interrupt programming ) I chose to wait for a period of no signal. My basic idea is this. Set two interrupts, one based on time, and one based on getting an IR signal. If the time interrupt goes off first then there was no IR if the IR interrupt goes off first then there is an IR signal. While I am waiting for the interrupt I can have the processor do something else.
Setup: Turn off the timer and the interrupts. Set the timer to 0. Set the expected state of the next IR signal ( it is normally high when there is not IR so the next expected is low. The next transition will also be on the falling edge so set that up. Make the IRState = IRSTATE_WAIT. Finally turn on the timer, turn on the interrupts and let the race begin.
The code:
Setup:[edit]
iRLow = true; clear_bit( option_reg, INTEDG ); // set rising edge clear falling edge clear_bit( intcon, INTF ); // the interrupt flag logIx = 0; // reset the log // turn off all the interrupts we are using clear_bit( pie1, TMR1IE ); // TMR1IE = timer 1 interrupt enable / set = enable clear_bit( pir1, TMR1IF ); // clear timer 1 interrupt flag bit clear_bit( intcon, INTE ); // INTE = external int erupt enable / set = enable clear_bit( intcon, INTF ); // INTF = external int erupt flag / clear = clear IRState = IRSTATE_WAIT; // clear timer0 flag and enable // set counter to something low ( do we need to mess with the prescaler ) clear_bit( t1con, TMR1ON ); // Timer1 On 1 = Enables Timer1 0 = Stops Timer1 tmr1l = 0; tmr1h = 0; set_bit( t1con, TMR1ON ); // Timer1 On 1 = Enables Timer1 0 = Stops Timer1 // now turn them both on and let the race begin set_bit( pie1, TMR1IE ); // TMR1IE = timer 1 interrupt enable / set = enable set_bit( intcon, INTE ); // INTE = external interrupt enable / set = enable
Interrupt:[edit]
We need to use our state variables, flags etc to distinguish between the earlier interrupt and then to figure out who won the race. If the signal was absent we set up for the ready state. If the signal was present we enter the busy and turn off the interrupts.
if ( ( test_bit( intcon, INTE ) ) && ( test_bit( intcon, INTF ) ) ) { clear_bit( intcon, INTF ); // the flag if ( ( IRState == IRSTATE_READY ) || ( IRState == IRSTATE_READ )) { ..... this is the code we have already covered } } else { // should be state wait // bad, we got ir before time out IRState = IRSTATE_BUSY; ...... both interrupts off } } //Handle timer1 interrupt if( pir1 & (1<<TMR1IF) ) { // timer interrupted before ir began IRState = IRSTATE_READY; clear_bit( pie1, TMR1IE ); // done with the time interrupt clear_bit( intcon, INTF ); // the flag set_bit( intcon, INTE ); // inte = interrupt enable, think just rb0, set = enable } return;
Note: not show here ( but present in the full code ) is the use of the global interrupt enable. It is one instruction that can turn off all interrupts. It has to be turned on before any interrupts will take place. Also in the full code an interrupt is used to support the RS232 receiver.
Getting Out[edit]
My way of detecting the end of transmission is to wait a while for the IR signal to "go out". How do I do this? It is based on another race between the RB0 and Timer interrupt. If the Timer goes off then the IR signal is out and the signal is over. As a slight additional trick I do not start the timer at 0 so I still control how long the delay of no IR is. So after checking why we are in the timer interrupt:
clear_bit( intcon, INTE ); // inte = interrupt enable, think just rb0, set = enable clear_bit( pie1, TMR1IE ); // TMR1IE = timer 1 interrupt enable set = eanble IRState = IRSTATE_GOT; serial_printf( "irstate=got\r" );
Breaking Out[edit]
On receiving the ! character the program should "break out" of the reading IIR routine. The problem is that it is not really in a routine, it is almost always waiting for interrupts. The key is to get the interrupts off. That is pretty much it. Do not turn on again untill you are all set up.