Embedded C track

Lesson 10 · Part 2: 8051 (Keil C51 / AT89C52)

External & Timer Interrupts

`interrupt N`

Part 2: 8051 (Keil C51 / AT89C52)7 min readFree lesson

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 IE registers (EA/EX0/ET0, etc.).
  • Write ISRs in Keil C51 using void isr(void) interrupt N syntax.
  • Configure external interrupt (INT0) and timer interrupts separately.
  • Understand why volatile is 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

  1. Project → New µVision Project… → name it c51_interrupt.
  2. Device: Atmel → AT89C52. Include STARTUP.A51 (Yes).
  3. Add main.c and then copy and paste the code below.
  4. Options for Target → Debug → Use Simulator.
  5. Rebuild(F7) → Start Debug(Ctrl+F5).
  6. Toggle P3.2 from 1→0 with Peripherals → I/O-Ports → Port 3 (trigger external interrupt), observe the LED in Port 1. You can also see the interrupt status in Peripherals → 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 3 window (simulate button press).
  • Expected behavior: Each time P3.2 falls, int0_isr is called and the P1.0 LED toggles. ON↔OFF changes every time the button is pressed. If you add press_count to 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 with EA = 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 volatile to variables shared between ISR↔main, and keep the ISR short.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/c51_interrupt/main.cC
#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.*/
    }
}
src/c51_interrupt/pc_test.cC
#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;
}

Assignment

Question 1 is for C51 (AT89C52, μVision simulator), and Question 2 is for pure logic (PC gcc verification).


Problem 1 — Blinking LED with Timer0 Interrupt

  • File name: homework_01/main.c
  • Key concepts: Timer interrupt (interrupt 1), ISR reload, IE setting
  • Platform: C51 (AT89C52)

Requirements

  • Set Timer0 in Mode1 to generate an overflow interrupt every 50ms.
  • In ISR (interrupt 1), reload TH0/TL0 and toggle the P1.0 LED.
  • Keep main as interrupt enabled only with an empty loop.

Expected Operation (Simulator)

  • The P1.0 LED should toggle at approximately 50ms intervals. Operate solely using interrupts without polling.

Hint

  • void timer0_isr(void) interrupt 1 { TH0=0x3C; TL0=0xB0; LED=!LED; }.
  • In main, set TMOD, TH0/TL0, ET0=1, EA=1, TR0=1 in order.
  • Mode1 does not have auto-reload, so reload each time in the ISR.

Problem 2 — Counting Overflows to Toggle Every 0.5s (PC)

  • File name: homework_02/main.c
  • Key concepts: Accumulating interrupt count, prescaling
  • Platform: Common (PC gcc)

Requirements

  • Assume 1 overflow = 50ms. Toggle LED every 10 overflows (=500ms).
  • From cycles 1 to 30, output overflow N (M ms): LED -> ON/OFF at the point where toggling occurs.
  • Start with the LED OFF. Output format must match “Expected Output” character by character.

Expected output

overflow 10 ( 500 ms): LED -> ON
overflow 20 (1000 ms): LED -> OFF
overflow 30 (1500 ms): LED -> ON

Hint

  • Toggle and output only when i % 10 == 0 occurs.
  • Elapsed time (ms) = i * 50. Format should be aligned with %2d, %4d.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <reg52.h>

/*
 *Answer 1 (C51, AT89C52) — LED blinking with Timer0 interrupt
 *
 *Key Points:
 *- ISR (interrupt 1) is called every time Timer0 overflows (50ms).
 *- Since Mode1 is not auto-reload, TH0/TL0 must be reloaded inside the ISR.
 *- The main function is free while only keeping interrupts enabled (polling unnecessary).
 *
 *Common mistake:
 *- If reload is missed inside the ISR, the next cycle becomes ~65.5ms instead of 65ms.
 *- If EA (global enable) is not turned on, the ISR will never be called.
 *- Confusing timer interrupt number (1) with external interrupt number (0).
 *
 *Expected behavior (simulator): P1.0 LED toggles at roughly 50ms intervals, main is an empty loop.
 */

sbit LED = P1 ^ 0;

void timer0_isr(void) interrupt 1
{
    TH0 = 0x3C;            /*65536 - 50000 = 0x3CB0 reload*/
    TL0 = 0xB0;
    LED = (bit)!LED;       /*Toggle every 50ms*/
}

void main(void)
{
    TMOD &= 0xF0;
    TMOD |= 0x01;          /* Timer0 = Mode 1 */
    TH0 = 0x3C;
    TL0 = 0xB0;

    ET0 = 1;               /*Enable Timer0 interrupt*/
    EA  = 1;               /*Enable global interrupt*/
    TR0 = 1;               /*Start Timer0*/

    while (1) {
        /*Blinking is handled by the interrupt. The main function can do other tasks.*/
    }
}
homework_02/main.cC
#include <stdio.h>

/*
 *Answer 2 (Pure Logic, PC) — Count timer overflows and toggle the LED every 0.5 seconds
 *
 *Key Points:
 *- 1 overflow = 50ms. Toggle the LED every 10 overflows, which equals 500ms.
 *- The interrupt counts accumulated overflows and operates at a certain count, a typical pattern.
 *
 *Common mistake:
 *- Toggling at every overflow results in a 50ms period (the desired period is 500ms).
 */
int main(void)
{
    unsigned char led = 0;     /*Start with 0=OFF*/
    int i;

    for (i = 1; i <= 30; i++) {
        if (i % 10 == 0) {                 /*Toggle every 10 times (=500ms)*/
            led = (unsigned char)!led;
            printf("overflow %2d (%4d ms): LED -> %s\n",
                   i, i * 50, led ? "ON" : "OFF");
        }
    }

    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now