 |
INTERRUPTS
An interrupt is some event that interrupts normal program
execution. Program flow is always sequential, being altered only by
those instructions that explicitly 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 suspend ed and control passed to a
special section of code that presumably would execute some function
related to the event that occurred. Once completed, 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 over
flown, whether we had received another character via the serial
port, or if some external event had occurred. Besides making the
main program hideous and hard to read, such a situation would make
our program inefficient since we'd be burning precious
"instruction cycles" ,checking for events that usually
don't happen.
For example, let's say we have a large 16k program executing many
subroutines Performing many tasks. Let's also suppose that we want
our program to automatically toggle the P3.0 port every time timer 0
overflows. The code for this is:
JNB TF0, SKIP_TOGGLE
CPL P3.0
CLR TF0
SKIP_TOGGLE:....
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, let's 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 we're 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
we've spent 1312 instruction cycles so 2.002% of our time is being
spent just checking when to toggle P3.0 and our code is hideous
because we have to make that check every iteration of our main
program loop.
Actually, this isn't 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, we'll notice the CLR TFO command has disappeared. That's
because when the 8051 executes our "timer 0 interrupt
routine," it automatically clears the TFO flag. We'll 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. Programmer must always end his 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 we've accomplished the same goal as the
first example that required 1312 instruction cycles. As far as the
toggling ofP3.0 goes, our code is 437 times more efficient! Not to
mention it's much easier to read and understand because we don't
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 it's
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.
INTERRUPTS TRIGGERING BY EVENTS
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 table 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.
SETTING UP INTERRUPTS
By default at power up, all interrupts are disabled. This means
that even if, for example, the TF0 bit is set, the 8051 will not
execute the interrupt. Program must specifically tell the 8051 that
it wishes to enable interrupts and specifically which interrupts it
wishes to enable.
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 we can see, each of the 8051's interrupts has its own bit in
the IE SFR. Programmer can enable a given interrupt by setting the
corresponding bit. For example, if he wishes to enable Timer 1
Interrupt, he 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, programmer must also set bit 7 of IE. Bit 7, the Global
Interrupt 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
programmer have time-critical code that needs to execute. In this
case, programmer may need the code to execute from start to finish
without any interrupt getting in the way. To accomplish this
programmer can simply clear bit 7 of IE (CLR EA) and then set it.
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).
POLLING SEQUENCE
The 8051 automatically evaluates whether or not an interrupt has
occured after each 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.
INTERRUPT PRIORITIES
The 8051 offer two levels of interrupt priority: high and low.
By using interrupt priorities programmer may assign higher priority
to certain interrupt conditions.
For example, programmer may have enabled Timer 1 Interrupt that is
automatically called every time Timer 1 overflows. Additionally,
programmer may have enabled the Serial Interrupt that is called
every time a character is received via the serial port. However,
programmer may consider that receiving a character is much more
important than the timer interrupt. In this case, if Timer 1
Interrupt is already executing, programmer may wish that the serial
interrupt must interrupt the Timer 1 Interrupt. When the serial
interrupt is complete, controls passes back to Timer 1 Interrupt and
finally back to the main program. Programmer 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 that is serviced first by
Polling sequence will be executed first.
START OF AN INTERRUPT
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.
- Interrupt 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 microcountroller
automatically clears the interrupt flag before passing control to
interrupt handler routine.
END OF AN INTERRUPT
An interrupt ends, when 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
Serial Interrupts are slightly different than the rest of the
interrupts. This is due to the fact that there is two interrupt
flags: RI and TI. If either flag is set, a serial interrupt is
triggered. 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 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, programmer's 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 programmer must clear these bits in his interrupt
handler. .
A brief code example is in order:
| INT_SERIAL: |
JNB RI, CHECK_TI MOV
A, SBUF CLR RI |
IF the RI flag is not set, we
jump to check TI
IF we got to this line, it's because the RI
bit *was* set |
| CHECK_TI: |
JNB TI, EXIT_INT CLR TI
MOV SBUF, #'A' |
Clear the RI bit after we've
processed it
IF the TI flag is not set, we jump to the
exit point |
| EXIT_INT: |
RETI |
Clear the TI bit before we send
another character
Send another character to the serial port |
As we 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 programmer forgets to clear the
interrupt bits, the serial interrupt will be executed over and over
until he clears 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 isn't
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 occurred. 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 additional1h 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 doesn't make
sense. A programmer that was unfami1iar 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. It means if programmer's 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
In this interrupt routine the MOV instruction and the ADD
instruction 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 heart's 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, programmers 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 programmer is absolutely sure of what he is
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
interrupts.
Note: An assembler will not allow to execute the instruction:
PUSH R0
This is due to the fact that depending on which register bank is
selected, R0 may refer to internal ram address 00h, 08h, 10h, or
l8h. R0, in and of itself, is not a valid memory address that the
PUSH and POP instructions can use.
Thus, if programmer is using any "R" register in his
interrupt routine, he will have to push that register's absolute
address onto the stack instead of just saying PUSH R0.
For example, instead of PUSH RO he would execute:
PUSH 00H I
COMMON PROBLEMS WITH INTERRUPTS I
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 programmer is using interrupts and his program is crashing or
does not seem to performing as he would expect, he should always
review the following interrupt-related issues.
REGISTER PROTECTION
Make sure to protecting all registers. If programmer forgets to
protect a register that 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 = 5Ih. programmer witness problems with registers changing
values unexpectedly or operations producing "incorrect"
values, it is very likely that he has forgotten to protect
registers. ALWAS PROTECT REGISTERS.
FORGETTING TO RESTORE PROTECTED VALUES
Another common error is to push registers onto the stack to
protect them, and then forget pop them off the stack before exiting
the interrupt. For example, programmer 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 he forgot to
restore the value of "B", an extra value remains on the
stack. When programmer executes the RETI instruction the 8051 will
use that value as the return address instead of the correct value.
In this case, his program will almost certainly crash. ALWAYS MAKE
SURE POP THE SAME NUMBER OF VALUES OFF THE STACK AS PUSHED ONTO IT.
USING RET INSTEAD OF RETI:
Remember that interrupts are always terminated with the RETI
instruction. It is easy inadvertently use the RET instruction
instead. However, the RET instruction will not end interrupt.
Usually, using a RET instead of a RETI will cause the illusion of
main program running normally, but interrupt will only be executed
once. If it appears that interrupt mysteriously stops executing,
verify the exit with RETI.
READING THE SERIAL PORT
Reading data received by the serial port is equally easy. To
read a byte from the serial port one just needs to read the value
stored in the SBUF (99h) SFR after the 8051 has automatically set
the RI flag in SCON.
For example, if program wants to wait for a character to be received
and subsequently read it into the Accumulator, the following code
may be used:
JNB RI, $; Wait for the 8051 to set the RI flag
MOV A, SBUF; Read the character from the serial port
The first line of the above code segment waits for the 8051 to
set the RI flag; again, the 8051 sets the RI flag automatically when
it receives a character via the serial port. So as long as the bit
is not set the program repeats the "JNB" instruction
continuously.
Once the RI bit is set upon character reception the above condition
automatically fails and program flow falls through to the "MOV"
instruction that reads the value.
BACK |
 |