How to use interrupts in microcontrollers

Table of Contents

In this tutorial we will learn how to use external interrupts in PIC microcontrollers. We will go in depth on how to set it up in hardware, and how to configure it correctly within the program. For the examples we will use microcontrollers from the PIC16F family; in particular the PIC16F877A.

1. What are interrupts and when do you use them?

An external interrupt is an event that can cause the microcontroller to stop what it is doing and execute a special routine called an interrupt service routine (ISR). The interrupt is typically caused by a change in the state of an external device, such as a button press or a sensor reading. This allows for real-time responses, as the microcontroller can react to the event immediately.

When an interrupt occurs, the ISR is called, and any code that is inside the ISR will be run. Once the ISR is finished, the microcontroller will return to the main program, at the point where it left off. There are several types of interrupts in the PIC16F877A, such as:

  • External interrupts,
  • Timer 0 (and timer 1) overflow interrupt,
  • RB port change interrupt,
  • Serial port transmit/receive interrupts,
  • ADC/comparator interrupts,
  • Watchdog timer interrupt.

To make sure that interrupts dont happen in the wrong order, there is a priority listed to them; the external interrupt has the highest priority of them all, and that is the one we will focus on in this tutorial.

2. Configuring the interrupt settings

Most of the interrupts are configured in the INTCON register, which contains variable flag bits for the TMR0 register overflow, RB port change and external RB0/INT pin interrupts.

The following bits in the INTCON register are relevant to external interrupts:

  • GIE: Global Interrupt Enable bit. This bit enables or disables all interrupts.
  • PEIE: Peripheral Interrupt Enable bit. This bit enables or disables the peripheral interrupts (Timer 0, Timer 1, Serial Port, A/D Converter, and Comparator).
  • INTE: RB0/INT External Interrupt Enable bit. This bit enables or disables the external interrupt.

To set the interrupt settings, you write to the INTCON register. The following table shows the values you need to write to the INTCON register to enable or disable the different interrupts:

BitValueDescription
GIE1Enables all interrupts.
PEIE1Enables the peripheral interrupts.
INTE1Enables the external interrupt.

Once RB0 pin is declared an external interrupt pin, the external interrupt flag, INTF, changes to 1 anytime it becomes low. Consequently, the code inside the void interrupt function will execute since the Interrupt Service Routine (ISR) will be called. Proper knowledge of these concepts is essential for external interrupts function.

2.1 Interrupt Service Routine (ISR)

An ISR, or Interrupt Service Routine, is a special routine that is called when an interrupt occurs. The ISR typically performs the desired action, such as turning on an LED or reading a sensor value.

The ISR is located in the program memory. The following is an example of an ISR for the external interrupt:

				
					void interrupt ISR() { 
        // Perform the desired action. 
        INTCONbits.INTF = 0; // Clear the interrupt flag
    }
				
			

The ISR is called when the interrupt occurs. The microcontroller will automatically save the state of the registers before executing the ISR. Once the ISR is finished, the microcontroller will restore the state of the registers and return to the main program. It is of vital importance, that you clear the interrupt flag after doing the task, because if another interrupt appears, it will not interrupt the current interrupt, else it might go wrong.

Here are some of the things to keep in mind when writing an ISR:

  • The ISR should be as short as possible.
  • The ISR should not make any changes to the global variables.
  • The ISR should not call any functions that make changes to the global variables.
  • The ISR should not call any functions that can cause an interrupt.

2.2 Program code

For this example we will create a program that shows a counter that increments every time the main program is being interrupted by the external interrupt. In this particular example, it is not too different from having a button to increase the counter, however, you must understand that there is a fundamental difference between the two. With the external interrupt, it literally interrupts the program to do the ISR. We will reuse the program code, from one of our previous tutorials on interfacing with an LCD, to display the data.

An important notice; in the original code for LCDs, we used PORTB as 8-bit databus to interface with the LCD. However, we moved all this to PORTC, as RB0 is now the interrupt. In my program code, this was done by just writing “#define Lcd_Port PORTC”. Of course, it can also be changed to 4-bit mode, and only use RB4-RB7 as databus. If you are using any other microcontroller, please consult my page on interfacing with the LCD to learn in just a few steps how to set it up for your particular case.

				
					#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 20000000

// Configuration bits settings
#pragma config FOSC = HS        // External Oscillator (HS)
#pragma config WDTE = OFF       // Watchdog Timer Disabled
#pragma config PWRTE = ON       // Power-Up Timer Enabled
#pragma config BOREN = OFF      // Brown-out Reset Disabled
#pragma config LVP = OFF        // Low-Voltage Programming Disabled

// Pin definitions
#define Lcd_EN RD7
#define Lcd_RW RD6
#define Lcd_RS RD5

// Port definitions
#define Lcd_Port PORTC // Use PORTC for LCD data lines

// Function prototypes - See further down
void LCD_DataWrite(char dat);
void LCD_CommandWrite(char cmd);
void LCD_Initialize();
void LCD_String(const char* text);
void LCD_Clear();

volatile uint16_t counter = 0; // Declare a volatile counter variable

void __interrupt() ISR() {
    if (INTF) {
        counter++; // Increment the counter
        LCD_Clear(); // Clear the LCD screen
        char buffer[17]; // Buffer to hold the counter as a string
        sprintf(buffer, "Counter: %u", counter); // Format the counter value
        LCD_String(buffer); // Display the counter on the LCD
        INTF = 0; // Clear the RB0 external interrupt flag
    }
}

void main() {
    TRISB0 = 1; // Set RB0 as input for the external interrupt
    TRISC = 0x00; // Set PORTC as output for LCD data lines
    TRISD = 0x00; // Set PORTD as output
    PORTD = 0x00; // Clear PORTD initially

    INTCON = 0b11010000; // Enable Global Interrupt, Peripheral Interrupt, and RB0 External Interrupt, set flag to 0

    LCD_Initialize();

    while (1) {
        // Your main program logic here
    }
}

// Other functions remain the same as in the original code on:
// https://florisera.com/embedded-systems/connecting-pic16f877a-with-an-lcd-screen/

// Function to send data byte to the LCD (HD44780)
void LCD_DataWrite(char dat)
{
   Lcd_Port=dat;	  // Set the data on the PORT
   Lcd_RS=1;	      // Select the Register for sending data by pulling RS HIGH
   Lcd_RW=0;          // Select Write by pulling RW LOW
   Lcd_EN=1;	      // Send a High-to-Low Pulse at EN pin
   __delay_us(10);
   Lcd_EN=0;          // set EN pin back to LOW
   __delay_ms(2);     // Add a delay to ensure proper timing, and no need for Buffer check
}

// Function to send command to the LCD (HD44780)
void LCD_CommandWrite(char cmd)
{
   Lcd_Port=cmd;	  // Set the data on the PORT
   Lcd_RS=0;	      // Select the Register for sending a command by pulling RS LOW
   Lcd_RW=0;          // Select Write by pulling RW LOW
   Lcd_EN=1;	      // Send a High-to-Low Pulse at EN pin
   __delay_us(10);
   Lcd_EN=0;          // set EN pin back to LOW
   __delay_ms(2);     // Add a delay to ensure proper timing, and no need for Buffer check
}

// Function to initialize the LCD
void LCD_Initialize()
{
    __delay_ms(15);    // 15 ms start up time, as per datasheet
    LCD_CommandWrite(0x38); // "Function set": 8-bit data, 2-line display, 5x8 font. 0b00111000
    LCD_CommandWrite(0x0C); // "Display on/off": Display on, cursor off, blinking off. 0b00001100
    LCD_CommandWrite(0x01); // "Clear Display":  Clear display. 0b00000001
    LCD_CommandWrite(0x06); // "Entry mode set": cursor increments, no display shift. 0b00000110
}

void LCD_String(const char* text) {
    int charIndex = 0;

    while (text[charIndex] != '\0') { //continue incrementing untill it reaches the null
        LCD_DataWrite(text[charIndex]);

        // Check if the text is longer than 16 characters & does not have a null on the 17th character
        if (charIndex == 15 && text[16] != '\0') {
            LCD_CommandWrite(0xC0); // Set DDRAM address to second line
        }

        charIndex++;
    }
}

void LCD_Clear() {
    LCD_CommandWrite(0x01); // Clear display
    __delay_ms(2);
}
				
			
Share
Tweet
Share
Pin
Email
0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments