Montag, 10. November 2014

Interrupts: The Basics

Introduction

Writing programs for PCs rarely involves contact with the underlying hardware. The operating system provides a layer between the application programmer and the hardware (the processor). Special functionality, for example detecting keys being pressed, is normally provided through libraries.

On microcontrollers such as the ATMEGA, operating systems are rarely used. The firmware interacts directly with the functionality provided by the processor. One important feature of most (if not all) processors is the ability to interrupt the normal program flow if a special condition occurs.

One example: The ATMEGA8 has a pin called INT0. If the controller is set up correctly, pulling the INT0 pin low, for example with a switch that connects it to GND, the normal program flow is interrupted. The program directly jumps to a special function, the interrupt handler routine. If that function returns, the program flow continues where it was before the interrupt happened.

Another example of an interrupt is the overflow of an internal counter. Each interrupt condition has a function that is called whenever that condition occurs. The ATMEGA8 has 19 different interrupts. They are all listed on page 46 in the manual.

I wanted to have a very simple example to get some practice in how to use interrupts. My goal was to change the status (on/off) of a LED whenever the INT0 pin is pulled to ground (by a switch). The program consists of

  • setup of INT0 interrupt and LED output
  • endless loop that does nothing
  • interrupt handler that switches the LED state

INT0 Setup

Finding out how to set up things in the microcontroller can be tedious, if you're using a feature for the first time. It often involves setting/clearing special bits in special registers in a specific order. Which ones to set is documented in the manual and it is good to read relevant sections at some point. For INT0 setup, the key sections of the manual are summarized is this picture:
First, bits 0 and 1 in the MCU control register (MCUCR) have to be cleared to select a level trigger. That means the interrupt is generated whenever the INT0 pin is pulled down. If the interrupt handler routine is done, and the INT0 pin is still low, the interrupt will be triggered again. 
(The default values for both bits is 0, so clearing them could be skipped if one can somehow make sure that they haven't been set somewhere in the program)

Second, bit 6 in the General Interrupt Control Register GICR has to be set to 1 to enable the INT0 interrupt. 

Third the "sei" instruction has to be called, to enable interrupts. 

First Attempt

Here is the full code:

#define F_CPU 1000000
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

// interrupt handler for INT0
ISR(INT0_vect)
{
 PORTC ^= (1 << DDC0);   // swap the LED state
}

int main(void)
{
 // interrupt setup
 GICR  |= (1<<INT0);                  // enable INT0;
 MCUCR &= ~((1<<ISC01) | (1<<ISC00)); // INT0 low level trigger
 sei();              // enable global interrupt
 DDRD  = 0;          // all PD pins configured as input   
 PORTD = (1 << PD2); // enable pull-up on the INT0 pin    

  // LED setup
 DDRC  = (1 << DDC0); // enable PC0 as output
 PORTC = (1 << DDC0); // switch pin on (LED on)

  while(1)
 {
  // spin forever with 1 MHz
 }

  return 0 ;
}

After the setup of the INT0 interrupt, I've enabled the pull-up resistor for the corresponding pin (PD2). I encourage you to test what happens if this is not done.
In addition, PC0 (where the LED is connected to) is configured as output and turned on.
The program enters an endless while-loop and can only temporarily escape if an interrupt is triggered and the ISR(INT0_vect) function is called.

The hardware looks like this. The switch is realized by a piece of wire... and is a bit tricky to operate, but it works.

Second Attempt

This works, but maybe not as intended. As long as the switch is closed, the green LED blinks very fast, because the interrupt routine is called again and again. There are different ways to achieve a single change of the LED whenever the switch is closed once. One way is to prevent the interrupt handler routine to return as long as the the interrupt condition is still true:

ISR(INT0_vect) 
{
 PORTC ^= (1 << DDC0);   // swap the LED
 // prevent interrupt handler from finishing
 _delay_ms(100);              // not good !!!
 while(!(PIND & (1 << PD2))); // not good !!!
 _delay_ms(100);              // not good !!!
}

The additional delays are there to allow the signal to reach a stable state (stop bouncing), see my rotary encoder post. This one will works as expected. 
But there is a big downside. The run time of this interrupt handler routine is not determined. If one would do that in an application that does more things than only turning a LED on/off, holding the switch would freeze the complete program. No other interrupts can be triggered as long as the INT0 interrupt hanlder didn't return. As a general rule: interrupt handlers should be executed as fast as possible

Third Attempt

There are, again, many ways to solve this. I've tried this: Let the interrupt handler just change the state of a global variable. The state of that variable is then checked in the main loop:

uint8_t swap  = 0;
ISR(INT0_vect) 
{
 swap = 1; // make interrup handler as short as possible
}

The modified while loop:

 while(1)
 {
  if (swap == 1)
  {
   PORTC ^= (1 << DDC0); // swap the led state
   GICR  &= ~(1<<INT0);  // disable INT0
   _delay_ms(100); 
   while(!(PIND & (1 << PD2)));
   _delay_ms(100); 
   swap = 0;
  }
  set_sleep_mode(SLEEP_MODE_IDLE); // select the desired sleep mode
  GICR  |= (1<<INT0);  // enable INT0
  sleep_mode();        // go to sleep
 }

The interrupt should not be triggered again before the switch is opened. It can be deactivated by clearing the INT0 bit in the GICR register. INT0 is enabled again, just before going to IDLE state. INT0 is able to wake up the controller from IDLE state. IDLE state means, that the program flow is stopped and no further instruction is executed.

Final Notes

I am not an expert in this, and all these examples are probably not "best practice". But I hope they illustrates some points about interrupts and help others to get started without diving too deep into the ATMEGA8 manual.
I think that it quickly gets interesting if multiple interrupts are enabled. I have never done this but whenever I do, I plan to share my experiences.

Keine Kommentare:

Kommentar veröffentlichen