Pin Change Interrupt Example

Using interrupts with the ATTiny84

Most IoT sensors report their measures to the base station on a regular basis, like every 5 minutes or even as often as every second. Sometimes, however, it can be useful if the sensor can report to the base station based on external events, like a door being opened or something being removed from its regular location.

I had exactly such need when I was creating a sensor for my carport: besides measuring temperature and relative humidity in the air I wanted the sensor to report when the port was opened or closed. In the hardware design I wanted to use a reed switch and I had gotten my hands on some NO (normally open) type switches, so I needed a way of interrupting the ATTiny84 (which is my preferred MCU for making sensors) using the switch.

Although this article is written specifically for the ATTiny84, I am sure things are the same for the other Atmel AVR MCUs.

Interrupt pins

According to the data sheet the ATTiny84 can handle pin change interrupt on all port A pins – that is 8 concurrent pin change interrupts for this tiny fellow!

So, with physical pins 6 through 13 (for the DIP) being ready to receive external pin change interrupts continuing to use my favourite MCU should be no problem.

Using interrupt

In order to trigger an interrupt, the hardware must be set up correctly and the firmware should act on the correct state of the interrupt – besides, of course, to set up the internal interrupt handler to “listen” to the specified port.

A minimal sample program for handling interrupts on a single input is shown below. The program turns on an LED for two seconds every time an interrupt is caught.

#define DOOR_PIN 3 // D3
#define LED_PIN  2 // D2

static int interrupted;

ISR(PCINT0_vect) { interrupted = true; }

void handleInterrupt() {
  //Turn on LED for 2 seconds when an interrupt is caught
  digitalWrite(LED_PIN, HIGH);
 
  sleep(2000);
 
  digitalWrite(LED_PIN, LOW);
}

void setup() {
  interrupted = false;
 
  pinMode(DOOR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
 
  GIMSK |= _BV(PCIE0);   // Enable Pin Change Interrupts
  PCMSK0 |= _BV(PCINT7); // Use PA7 as interrupt pin
 
  sei(); //Enable interrupts
}

void loop() {
  if(interrupted) {
    handleInterrupt();

    interrupted = false;
  }
}

I will not go into details about ordinary Arduino programs, so it is expected that the reader knows about the basics of writing Arduino programs.

To handle interrupts a certain interface must be implemented in the program. This interface, basically tells the MCU which part of the program to execute when an interrupt is triggered on one of the PCINT handlers (PCINT0 or PCINT1 for the ATTiny84).

Because this part of the program lies outside the main loop it is necessary to declare a global variable that can be set when an interrupt is triggered and read by the main loop. In the program above this is the variable interrupted.

The part of the program that is called when an interrupt is caught is the ISR function, which takes as argument the interrupt vector for the PCINT handler used – in this case PCINT0 because the interrupt pin defined belongs to this PCINT handler. All the function does is set the global variable to let the main loop know that an interrupt has been caught.

In each iteration of the main loop the global variable, interrupted, is checked and if it is true a function, handleInterrupt(), is called, which will handle the actions required when an interrupt is received. In this case it turns on the LED for two seconds. It is important to reset the global variable to catch subsequent interrupts.

It might be possible to execute more light-weight tasks directly in the interrupt handler function, thus avoiding the global variable and the additional code in the main loop. However, I recommend keeping the complexity and resource consumption of the code in the interrupt handler function at a minimum as further detailed later in this article.

To tell the ATTiny to catch interrupts the setup() function must tell it where to listen and that it should enable its internal interrupt handling routines. In the code above, the global variable is first set to a negative state after which the pin modes of the interrupt and LED pins are set. Then pin change interrupt is enabled for the PCINT0 vector and the MCU is told to listen for pin change interrupts on PCINT7, which is connected to the D3 pin on which the door contact should be connected. Finally, the sei() function is called to start the internal interrupt handling.

As previously mentioned, care should be taken not to make the handleInterrupt() function too computationally heavy because in the time it runs no interrupts can be handled immediately – they will be caught and the global variable will be set to make the main loop call the function in its next iteration.

This is especially important if catching on both the rising and falling edges of the interrupt trigger signal (discrete on and off). Imagine, if the function is so computationally heavy that one could first close a door, which triggers the function and then open the door, go through it and close it again all while the initial call to the function is running – it will never be registered that the door was opened and closed the second time.

Of course not many operations take that long, but if the handleInterrupt() function hangs because it is trying to send information over a network that is either slow or of poor quality several seconds could pass making the scenario a reality.

Combining interrupt and sleep

In the example above I use the built in delay() function, which is not very battery friendly and should preferably be replaced by something better, e.g. the JeeLabs low-power alternative Sleepy – which, by the way, also use interrupts.

The biggest problem, however, with the delay() function is that it cannot be interrupted. This means that when a pin change interrupt is caught, the ongoing delay in the loop() function must run in its entirety and that the interrupt-specific code of the loop() function will not execute until its next scheduled iteration.

I will not cover getting started with the JeeLabs Sleepy module here, but will dive straight into combining it with pin change interrupts. If you are unfamiliar with the JeeLabs Sleepy module, I suggest that you familiarise with it at JeeLabs.

In fact, using the JeeLabs Sleepy module in combination with pin change interrupts is rather easy – it can be used in the usual way within the loop() function as shown in the code below.

#include <JeeLib.h> // https://github.com/jcw/jeelib

#define DOOR_PIN 3 // D3
#define LED_PIN  2 // D2

#define MINUTE   60 * 1000

static int interrupted;

ISR(WDT_vect) { Sleepy::watchdogEvent(); } // interrupt handler for JeeLabs Sleepy power saving

ISR(PCINT0_vect) { interrupted = true; } //Interrupt handler for door contact

void handleInterrupt() {
  //Turn on LED for 2 seconds when an interrupt is received
  digitalWrite(LED_PIN, HIGH);
 
  Sleepy::loseSomeTime(2000);
 
  digitalWrite(LED_PIN, LOW);
}

void setup() {
  interrupted = false;
 
  pinMode(DOOR_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
 
  GIMSK |= _BV(PCIE0); // Enable Pin Change Interrupts
  PCMSK0 |= _BV(PCINT7); // Use PA7 as interrupt pin
 
  sei(); //Enable interrupts
}

void loop() {
  if(interrupted) {
    handleInterrupt();

    interrupted = false;
  }

  //Sleep in low-power state for 60 seconds
  //Pin change interrupt will cause wake up
  Sleepy::loseSomeTime(MINUTE); 
}

Besides being interruptible by the pin change interrupts the Sleepy module has the added feature of not just halting operation for an amount of time, but it also puts the MCU to sleep while doing so – and wakes it up again when an interrupt is caught. When the MCU is asleep it uses a minimum of power, thus preserving valuable battery life.

Conclusion

For most sensor projects interaction is very limited and the sensors usually just sit there and mind their own business, but in some cases it might be a requirement that the sensor reports on more than, say, environmental figures. Being able to immediately act on external inputs without consuming unnecessary power in an active wait state is key to more advanced sensors and other MCU-controlled thingies that require some kind of external interaction – be it a push button for deliberate action or a door contact for surveillance.

Using the ATTiny84 with very limited additional configuration enables such uses on a very low-cost scale in both hardware and development.

0 comments on “Using interrupts with the ATTiny84Add yours →

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: