Polling delays in Chapter 9 are accurate, but the CPU gets stuck while waiting. If you poll a button, you have to keep checking it. Interrupts flip this problem. Normally, the main code does its job, but when a certain event (button press, timer overflow) occurs, the CPU briefly steps in to execute a designated function (ISR) and then returns to its original task. This method of "reacting only when an event occurs" is a core paradigm in embedded systems.
This chapter covers the 8051's external interrupt (INT0) and timer interrupt. You will learn how to enable interrupts using the IE registers, write ISRs using Keil C51's interrupt N syntax, and understand why volatile is needed for variables shared between the ISR and the main code.
Learning Objectives
- Explain the concept of interrupts (ISR, vector, asynchronous events) by comparing them with polling.
- Allow interrupts using
IEregisters (EA/EX0/ET0, etc.). - Write ISRs in Keil C51 using
void isr(void) interrupt Nsyntax. - Configure external interrupt (INT0) and timer interrupts separately.
- Understand why
volatileis attached to variables shared between the ISR and main code.
Target Environment
- Compiler: Keil C51.
- Device: AT89C52 (μVision simulator).
- Verification: μVision simulator (trigger INT0 via Port 3, observe LEDs on Port 1). ISR logic with PC
gcc(pc_test.c).
Key Concepts
1) What is an interrupt
When an event occurs, the hardware automatically stops the current code and jumps to a function (ISR, Interrupt Service Routine) at a predefined address (interrupt vector). After the ISR finishes, it returns to the point where it stopped.
| Polling | Interrupt | |
|---|---|---|
| Event detection | Continually check | Automatically called when it occurs |
| CPU efficiency | Low (wastes waiting) | High (free most of the time) |
| Response delay | Depends on loop cycle | Immediate |
2) 8051 Interrupt Sources and Numbers
Keil C51 assigns a number to each interrupt (used in ISR declaration).
| Number | Source | Flag |
|---|---|---|
| 0 | External Interrupt 0 (INT0, P3.2) | IE0 |
| 1 | Timer 0 Overflow | TF0 |
| 2 | External Interrupt 1 (INT1, P3.3) | IE1 |
| 3 | Timer 1 Overflow | TF1 |
| 4 | Serial (UART) | RI/TI |
3) IE — Interrupt Enable Register
| Bit | Name | Function |
|---|---|---|
| EA | Global enable | If 0, blocks all interrupts (master switch) |
| ES | Serial | |
| ET1 / ET0 | Timer1 / Timer0 | |
| EX1 / EX0 | External1 / External0 |
Rule: Turn on individual interrupts (e.g., EX0 = 1), and finally turn on the global EA = 1. If EA is 0, nothing will work regardless of what is enabled.
4) External Interrupt Trigger Mode — Level vs Edge
Select the INT0 trigger method using IT0 (TCON bit).
IT0 = 0: Level trigger (remains active while the pin is Low). If you keep the button pressed, the ISR can repeat.IT0 = 1: Falling-edge trigger (once at the moment High→Low). Suitable for the moment the button is "pressed."
5) ISR Syntax and volatile
In Keil C51, an ISR has no return value or parameters and uses interrupt N.
void int0_isr(void) interrupt 0 /*External Interrupt 0*/
{
/*Short and fast! Avoid heavy tasks like printf*/
}
If the ISR and main use the same variable, the compiler may mistakenly cache it, thinking "this variable won’t change" from the main's perspective. Since the ISR changes it asynchronously, attach volatile to shared variables to read from memory each time.
volatile unsigned char press_count; /*ISR ↔ main shared*/
Keep ISRs short. Heavy tasks (delays, UART transmission, etc.) should just set a flag and be handled in the main loop.
Creating a μVision Project
Project → New µVision Project…→ name itc51_interrupt.- Device: Atmel → AT89C52. Include STARTUP.A51 (Yes).
- Add
main.cand then copy and paste the code below. Options for Target → Debug → Use Simulator.- Rebuild(F7) → Start Debug(Ctrl+F5).
- Toggle P3.2 from 1→0 with
Peripherals → I/O-Ports → Port 3(trigger external interrupt), observe the LED inPort 1. You can also see the interrupt status inPeripherals → Interrupt.
Examples
Example 1 — main.c: Handling Buttons via External Interrupt
When INT0 (P3.2) falls, the ISR toggles the LED. The main loop is empty.
#include <reg52.h>
/*
*Lesson 10 Example 1 — Handling a Button with External Interrupt (INT0).
*Target: AT89C52, Compiler: Keil C51.
*
*Polling (8 cases) requires main to continuously monitor the button. Interrupts, on the other hand, the button
*The CPU briefly intervenes to execute the ISR only at the 'moment' it is pressed, and normally the main
*It is free. When the INT0 pin (P3.2) has a falling edge, int0_isr() is called.
*/
sbit LED = P1 ^ 0; /*Active-low LED*/
/*Variables used together by ISR and main are volatile (to prevent compiler caching).*/
volatile unsigned char press_count = 0;
/*External Interrupt 0 Service Routine: interrupt 0 (Keil C51 syntax).*/
void int0_isr(void) interrupt 0
{
press_count++; /*Accumulated number of presses*/
LED = (bit)!LED; /*Toggle LED*/
}
void main(void)
{
IT0 = 1; /*Trigger INT0 on the 'falling edge' (not level)*/
EX0 = 1; /*Enable External Interrupt 0*/
EA = 1; /*Allow global interrupts (master switch)*/
while (1) {
/*The main is free. The button handling is taken care of by the interrupt.
*If necessary, you can perform other actions here with press_count.*/
}
}
Example 2 — pc_test.c: Verifying ISR Logic
Check on the PC the count and LED status after the interrupt occurs N times.
#include <stdio.h>
/*
*Lesson 10 Example 2 — Verify on the PC what the ISR does (increment count + toggle LED).
*In reality, interrupts occur asynchronously, but here, we apply 'interrupt N times'
*sequentially to check the final state (press_count, LED).
*Active low, so led variable 0 = ON, 1 = OFF.
*/
int main(void)
{
unsigned char led = 1; /*Start: OFF (1=OFF)*/
unsigned int count = 0;
int events = 5; /*Assuming 5 interrupts occur*/
int e;
for (e = 1; e <= events; e++) {
count++;
led = (unsigned char)!led;
printf("IRQ %d: count=%u led=%s\n",
e, count, led ? "OFF" : "ON");
}
return 0;
}
Checking with Simulator
- After running (F5), change P3.2 from 1→0 in the
Port 3window (simulate button press). - Expected behavior: Each time P3.2 falls,
int0_isris called and the P1.0 LED toggles. ON↔OFF changes every time the button is pressed. If you addpress_countto Watch, it increases by 1 each time the button is pressed.
Verification on a PC
gcc pc_test.c -o pc_test && ./pc_test
Expected output (stdout):
IRQ 1: count=1 led=ON
IRQ 2: count=2 led=OFF
IRQ 3: count=3 led=ON
IRQ 4: count=4 led=OFF
IRQ 5: count=5 led=ON
Common Mistakes
Q. The ISR is not called at all.
A. Most likely, EA = 1 (global enable) is not turned on. Individual enables (EX0/ET0, etc.) are not enough; the master switch EA must be on. Also, make sure the ISR's interrupt N number matches the source (External 0=0, Timer 0=1).
Q. A variable changed in the ISR does not seem to change in main.
A. You probably forgot to attach volatile to the shared variable. If the compiler caches that variable in main, it cannot see changes made in the ISR. Attach volatile.
Q. I pressed the button once, but the ISR is called multiple times.
A. If IT0 = 0 (level-triggered) is used, it keeps triggering while the button is pressed. If you want it to trigger only once at the moment of pressing, change it to IT0 = 1 (edge-triggered). If contact bounce (chattering) is the cause, debouncing (part 21) is needed.
Q. The system lags when doing long tasks inside the ISR.
A. ISRs should be short. Other interrupts are delayed during an ISR, and main stops. Heavy tasks should only set a volatile flag in the ISR, and the main loop should handle the processing by checking that flag.
Summary
- Interrupts execute the ISR only when an event occurs, eliminating the waiting waste of polling.
- Turn on individual bits of
IE(EX0/ET0…) and enable globally at the end withEA = 1. - In Keil C51, ISRs have the form
void isr(void) interrupt N(External 0=0, Timer 0=1…). - For external interrupts, select the Level/Edge trigger with
IT0. - Attach
volatileto variables shared between ISR↔main, and keep the ISR short.
Try it yourself
Solve the assignment below. The reference solution is shown below.