As the name implies, an
interrupt is some event which interrupts normal program
execution.
As stated earlier, program flow is always
sequential, being altered only by those instructions which
expressly cause program flow to deviate in some way. However,
interrupts give us a mechanism to "put on hold" the normal
program flow, execute a subroutine, and then resume normal
program flow as if we had never left it. This subroutine, called
an interrupt handler, is only executed when a certain event
(interrupt) occurs. The event may be one of the timers
"overflowing," receiving a character via the serial port,
transmitting a character via the serial port, or one of two
"external events." The 8051 may be configured so that when any
of these events occur the main program is temporarily suspended
and control passed to a special section of code which presumably
would execute some function related to the event that occured.
Once complete, control would be returned to the original
program. The main program never even knows it was interrupted.
The ability to interrupt normal program
execution when certain events occur makes it much easier and
much more efficient to handle certain conditions. If it were not
for interrupts we would have to manually check in our main
program whether the timers had overflown, whether we had
received another character via the serial port, or if some
external event had occured. Besides making the main program ugly
and hard to read, such a situation would make our program
inefficient since wed be burning precious "instruction cycles"
checking for events that usually dont happen.
For example, lets say we have a large 16k
program executing many subroutines performing many tasks. Lets
also suppose that we want our program to automatically toggle
the P3.0 port every time timer 0 overflows. The code to do this
isnt too difficult:
Since the TF0 flag is set whenever timer 0
overflows, the above code will toggle P3.0 every time timer 0
overflows. This accomplishes what we want, but is inefficient.
The JNB instruction consumes 2 instruction cycles to
determine that the flag is not set and jump over the unnecessary
code. In the event that timer 0 overflows, the CPL and CLR
instruction require 2 instruction cycles to execute. To make the
math easy, lets say the rest of the code in the program requires
98 instruction cycles. Thus, in total, our code consumes 100
instruction cycles (98 instruction cycles plus the 2 that are
executed every iteration to determine whether or not timer 0 has
overflowed). If were in 16-bit timer mode, timer 0 will overflow
every 65,536 machine cycles. In that time we would have
performed 655 JNB tests for a total of 1310 instruction
cycles, plus another 2 instruction cycles to perform the code.
So to achieve our goal weve spent 1312 instruction cycles. So
2.002% of our time is being spent just checking when to toggle
P3.0. And our code is ugly because we have to make that check
every iteration of our main program loop.
Luckily, this isnt necessary. Interrupts let us
forget about checking for the condition. The microcontroller
itself will check for the condition automatically and when the
condition is met will jump to a subroutine (called an interrupt
handler), execute the code, then return. In this case, our
subroutine would be nothing more than:
CPL P3.0
RETI
First, youll notice the CLR TF0 command has
disappeared. Thats because when the 8051 executes our "timer 0
interrupt routine," it automatically clears the TF0 flag. Youll
also notice that instead of a normal RET instruction we
have a RETI instruction. The RETI instruction does the
same thing as a RET instruction, but tells the 8051 that an
interrupt routine has finished. You must always end your
interrupt handlers with RETI.
Thus, every 65536 instruction cycles we execute
the CPL instruction and the RETI instruction. Those two
instructions together require 3 instruction cycles, and weve
accomplished the same goal as the first example that required
1312 instruction cycles. As far as the toggling of P3.0 goes,
our code is 437 times more efficient! Not to mention its much
easier to read and understand because we dont have to remember
to always check for the timer 0 flag in our main program. We
just setup the interrupt and forget about it, secure in the
knowledge that the 8051 will execute our code whenever its
necessary.
The same idea applies to receiving data via the
serial port. One way to do it is to continuously check the
status of the RI flag in an endless loop. Or we could check the
RI flag as part of a larger program loop. However, in the latter
case we run the risk of missing characters--what happens
if a character is received right after we do the check, the rest
of our program executes, and before we even check RI a
second character has come in. We will lose the first character.
With interrupts, the 8051 will put the main program "on hold"
and call our special routine to handle the reception of a
character. Thus, we neither have to put an ugly check in our
main code nor will we lose characters.
What Events Can Trigger
Interrupts, and where do they go?
We can configure the 8051 so that any of the
following events will cause an interrupt:
Timer 0 Overflow.
Timer 1 Overflow.
Reception/Transmission of Serial Character.
External Event 0.
External Event 1.
In other words, we can configure the 8051 so
that when Timer 0 Overflows or when a character is
sent/received, the appropriate interrupt handler routines are
called.
Obviously we need to be able to distinguish
between various interrupts and executing different code
depending on what interrupt was triggered. This is accomplished
by jumping to a fixed address when a given interrupt occurs.
Interrupt
Flag
Interrupt Handler Address
External 0
IE0
0003h
Timer 0
TF0
000Bh
External 1
IE1
0013h
Timer 1
TF1
001Bh
Serial
RI/TI
0023h
By consulting the above chart we see that
whenever Timer 0 overflows (i.e., the TF0 bit is set), the main
program will be temporarily suspended and control will jump to
000BH. It is assumed that we have code at address 000BH that
handles the situation of Timer 0 overflowing.
By default at powerup, all interrupts are
disabled. This means that even if, for example, the TF0 bit is
set, the 8051 will not execute the interrupt. Your program must
specifically tell the 8051 that it wishes to enable interrupts
and specifically which interrupts it wishes to enable.
Your program may enable and disable interrupts
by modifying the IE SFR (A8h):
Bit
Name
Bit Address
Explanation of Function
7
EA
AFh
Global Interrupt
Enable/Disable
6
-
AEh
Undefined
5
-
ADh
Undefined
4
ES
ACh
Enable Serial Interrupt
3
ET1
ABh
Enable Timer 1 Interrupt
2
EX1
AAh
Enable External 1 Interrupt
1
ET0
A9h
Enable Timer 0 Interrupt
0
EX0
A8h
Enable External 0 Interrupt
As you can see, each of the 8051s interrupts has
its own bit in the IE SFR. You enable a given interrupt by
setting the corresponding bit. For example, if you wish to
enable Timer 1 Interrupt, you would execute either:
MOV IE,#08h
or
SETB ET1
Both of the above instructions set bit 3 of IE,
thus enabling Timer 1 Interrupt. Once Timer 1 Interrupt is
enabled, whenever the TF1 bit is set, the 8051 will
automatically put "on hold" the main program and execute the
Timer 1 Interrupt Handler at address 001Bh.
However, before Timer 1 Interrupt (or any other
interrupt) is truly enabled, you must also set bit 7 of IE. Bit
7, the Global Interupt Enable/Disable, enables or disables all
interrupts simultaneously. That is to say, if bit 7 is cleared
then no interrupts will occur, even if all the other bits of IE
are set. Setting bit 7 will enable all the interrupts that have
been selected by setting other bits in IE. This is useful in
program execution if you have time-critical code that needs to
execute. In this case, you may need the code to execute from
start to finish without any interrupt getting in the way. To
accomplish this you can simply clear bit 7 of IE (CLR EA) and
then set it after your time-criticial code is done.
So, to sum up what has been stated in this
section, to enable the Timer 1 Interrupt the most common
approach is to execute the following two instructions:
SETB ET1
SETB EA
Thereafter, the Timer 1 Interrupt Handler at
01Bh will automatically be called whenever the TF1 bit is set
(upon Timer 1 overflow).
The 8051 automatically evaluates whether an
interrupt should occur after every instruction. When checking
for interrupt conditions, it checks them in the following order:
External 0 Interrupt
Timer 0 Interrupt
External 1 Interrupt
Timer 1 Interrupt
Serial Interrupt
This means that if a Serial Interrupt occurs at
the exact same instant that an External 0 Interrupt occurs, the
External 0 Interrupt will be executed first and the Serial
Interrupt will be executed once the External 0 Interrupt has
completed.
The 8051 offers two levels of interrupt
priority: high and low. By using interrupt priorities you may
assign higher priority to certain interrupt conditions.
For example, you may have enabled Timer 1
Interrupt which is automatically called every time Timer 1
overflows. Additionally, you may have enabled the Serial
Interrupt which is called every time a character is received via
the serial port. However, you may consider that receiving a
character is much more important than the timer interrupt. In
this case, if Timer 1 Interrupt is already executing you may
wish that the serial interrupt itself interrupts the Timer 1
Interrupt. When the serial interrupt is complete, control passes
back to Timer 1 Interrupt and finally back to the main program.
You may accomplish this by assigning a high priority to the
Serial Interrupt and a low priority to the Timer 1 Interrupt.
Interrupt priorities are controlled by the IP
SFR (B8h). The IP SFR has the following format:
Bit
Name
Bit Address
Explanation of Function
7
-
-
Undefined
6
-
-
Undefined
5
-
-
Undefined
4
PS
BCh
Serial Interrupt Priority
3
PT1
BBh
Timer 1 Interrupt Priority
2
PX1
BAh
External 1 Interrupt Priority
1
PT0
B9h
Timer 0 Interrupt Priority
0
PX0
B8h
External 0 Interrupt Priority
When considering interrupt priorities, the
following rules apply:
Nothing can interrupt a high-priority
interrupt--not even another high priority interrupt.
A high-priority interrupt may interrupt a
low-priority interrupt.
A low-priority interrupt may only occur if no
other interrupt is already executing.
If two interrupts occur at the same time, the
interrupt with higher priority will execute first. If both
interrupts are of the same priority the interrupt which is
serviced first by polling sequence will be executed first.
When an interrupt is triggered, the following
actions are taken automatically by the microcontroller:
The current Program Counter is saved on the
stack, low-byte first.
Interrupts of the same and lower priority are
blocked.
In the case of Timer and External interrupts,
the corresponding interrupt flag is cleared.
Program execution transfers to the
corresponding interrupt handler vector address.
The Interrupt Handler Routine executes.
Take special note of the third step: If the
interrupt being handled is a Timer or External interrupt, the
microcontroller automatically clears the interrupt flag before
passing control to your interrupt handler routine. This means it
is not necessary that you clear the bit in your code.
An interrupt ends when your program executes the
RETI (Return from Interrupt) instruction. When the RETI
instruction is executed the following actions are taken by the
microcontroller:
Two bytes are popped off the stack into the
Program Counter to restore normal program execution.
Interrupt status is restored to its
pre-interrupt status.
Serial Interrupts are slightly different than
the rest of the interrupts. This is due to the fact that there
are two interrupt flags: RI and TI. If either flag is set, a
serial interrupt is triggered. As you will recall from the
section on the serial port, the RI bit is set when a byte is
received by the serial port and the TI bit is set when a byte
has been sent.
This means that when your serial interrupt is
executed, it may have been triggered because the RI flag was set
or because the TI flag was set--or because both flags were set.
Thus, your routine must check the status of these flags to
determine what action is appropriate. Also, since the 8051 does
not automatically clear the RI and TI flags you must clear these
bits in your interrupt handler.
A brief code example is in order:
INT_SERIAL:
JNB RI,CHECK_TI
;If the RI flag is not set,
we jump to check TI
MOV A,SBUF
;If we got to this line, its
because the RI bit *was* set
CLR RI
;Clear the RI bit after weve
processed it
CHECK_TI:
JNB TI,EXIT_INT
;If the TI flag is not set,
we jump to the exit point
CLR TI
;Clear the TI bit before we
send another character
MOV SBUF,#A
;Send another character to
the serial port
EXIT_INT:
RETI
As you can see, our code checks the status of
both interrupts flags. If both flags were set, both sections of
code will be executed. Also note that each section of code
clears its corresponding interrupt flag. If you forget to clear
the interrupt bits, the serial interrupt will be executed over
and over until you clear the bit. Thus it is very important that
you always clear the interrupt flags in a serial interrupt.
Important Interrupt
Consideration: Register Protection
One very important rule applies to all interrupt
handlers: Interrupts must leave the processor in the same state
as it was in when the interrupt initiated.
Remember, the idea behind interrupts is that the
main program isnt aware that they are executing in the
"background." However, consider the following code:
CLR C ;Clear carry MOV A,#25h ;Load the accumulator with 25h ADDC A,#10h ;Add 10h, with carry
After the above three instructions are executed,
the accumulator will contain a value of 35h.
But what would happen if right after the MOV
instruction an interrupt occured. During this interrupt, the
carry bit was set and the value of the accumulator was changed
to 40h. When the interrupt finished and control was passed back
to the main program, the ADDC would add 10h to 40h, and
additionally add an additional 1h because the carry bit is set.
In this case, the accumulator will contain the value 51h at the
end of execution.
In this case, the main program has seemingly
calculated the wrong answer. How can 25h + 10h yield 51h as a
result? It doesnt make sense. A programmer that was unfamiliar
with interrupts would be convinced that the microcontroller was
damaged in some way, provoking problems with mathematical
calculations.
What has happened, in reality, is the interrupt
did not protect the registers it used. Restated: An
interrupt must leave the processor in the same state as it was
in when the interrupt initiated.
What does this mean? It means if your interrupt
uses the accumulator, it must insure that the value of the
accumulator is the same at the end of the interrupt as it was at
the beginning. This is generally accomplished with a PUSH and
POP sequence. For example:
PUSH ACC
PUSH PSW
MOV A,#0FFh
ADD A,#02h
POP PSW
POP ACC
The guts of the interrupt is the MOV
instruction and the ADD instruction. However, these two
instructions modify the Accumulator (the MOV instruction) and
also modify the value of the carry bit (the ADD instruction will
cause the carry bit to be set). Since an interrupt routine must
guarantee that the registers remain unchanged by the routine,
the routine pushes the original values onto the stack using the
PUSH instruction. It is then free to use the registers it
protected to its hearts content. Once the interrupt has finished
its task, it pops the original values back into the registers.
When the interrupt exits, the main program will never know the
difference because the registers are exactly the same as they
were before the interrupt executed.
In general, your interrupt routine must protect
the following registers:
PSW
DPTR (DPH/DPL)
PSW
ACC
B
Registers R0-R7
Remember that PSW consists of many individual
bits that are set by various 8051 instructions. Unless you are
absolutely sure of what you are doing and have a complete
understanding of what instructions set what bits, it is
generally a good idea to always protect PSW by pushing
and popping it off the stack at the beginning and end of your
interrupts.
Note also that most assemblers (in fact, ALL
assemblers that I know of) will not allow you to execute the
instruction:
PUSH R0
This is due to the fact that depending on which
register bank is selected, R0 may refer to either internal ram
address 00h, 08h, 10h, or 18h. R0, in and of itself, is not a
valid memory address that the PUSH and POP instructions can use.
Thus, if you are using any "R" register in your
interrupt routine, you will have to push that registers absolute
address onto the stack instead of just saying PUSH R0.
For example, instead of PUSH R0 you would execute:
PUSH 00h
Of course, this only works if youve selected the
default register set. If you are using an alternate register
set, you must PUSH the address which corresponds to the register
you are using.
Interrupts are a very powerful tool available to
the 8051 developer, but when used incorrectly they can be a
source of a huge number of debugging hours. Errors in interrupt
routines are often very difficult to diagnose and correct.
If you are using interrupts and your program is
crashing or does not seem to be performing as you would expect,
always review the following interrupt-related issues:
Register Protection: Make sure you are
protecting all your registers, as explained above. If you
forget to protect a register that your main program is using,
very strange results may occur. In our example above we saw
how failure to protect registers caused the main program to
apparently calculate that 25h + 10h = 51h. If you witness
problems with registers changing values unexpectedly or
operations producing "incorrect" values, it is very likely
that youve forgotten to protect registers. ALWAYS PROTECT
YOUR REGISTERS.
Forgetting to restore protected values:
Another common error is to push registers onto the stack to
protect them, and then forget to pop them off the stack before
exiting the interrupt. For example, you may push ACC, B, and
PSW onto the stack in order to protect them and subsequently
pop only ACC and PSW off the stack before exiting. In this
case, since you forgot to restore the value of "B", an extra
value remains on the stack. When you execute the RETI
instruction the 8051 will use that value as the return address
instead of the correct value. In this case, your program will
almost certainly crash. ALWAYS MAKE SURE YOU POP THE SAME
NUMBER OF VALUES OFF THE STACK AS YOU PUSHED ONTO IT.
Using RET instead of RETI: Remember that
interrupts are always terminated with the RETI instruction. It
is easy to inadvertantly use the RET instruction instead.
However, the RET instruction will not end your interrupt.
Usually, using a RET instead of a RETI will cause the illusion
of your main program running normally, but your interrupt will
only be executed once. If it appears that your interrupt
mysteriously stops executing, verify that you are exiting with
RETI.