Interrupts for the Arduino Uno - More than you might think!

Are you looking to make a program that requires a bunch of interrupts using an ATmega328 or ATmega168 chip (such as on the Arduino Uno, Nano, or Mini platforms)?  If so, you may have been disappointed by the basic documentation you can find on this matter, and tempted to buy a more advanced Arduino such as the Mega2560, Zero, or even Due.  But have you seen how much their chips cost on Mouser?  If you're looking to do a small run of boards with what you ultimately produce, you will be taken aback to find that ATmega2560-16AU chips cost over 5x more than ATmega168A-AU chips!  In fact, I just recently bought an Arduino Mega2560 for less than what you can buy its chip for.  Having seen this, I knew there had to be a better way to leverage a cheaper chip.

The Problem, In Short


I need an application that can read from multiple sensor inputs, each of which will pulse with a high-frequency square wave (i.e. toggle on & off very rapidly) upon activation of the particular magnetic device they sit next to.

Given the sensors' behavior, and that there's no guarantee each sensor will produce more than one pulse if the underlying magnetic device does not stay on for a very long time, the best way to look for the changing state of the sensors is to use interrupts that can quickly capture exactly which pin just changed state, and then during the CPU's downtime (when it's not handling interrupts), it can go and take the appropriate action given which sensor(s) just pulsed.

Another problem to battle on the road to solving my problem!


By reading the standard documentation, you might be led to believe the Arduino Uno or any other ATmega328-based platform only has two useful interrupts, existing on digital pins 2 & 3.  Specifically, it says that the attachInterrupt() function only works on those two pins.  This, however, is misleading.  In fact, any of the ATmega's I/O pins can be used as interrupts -- it only really makes a difference if you need to use an external interrupt versus simply a pin change interrupt.

An external interrupt has the capability to fire upon a state transition, such as the rising edge or falling edge of a signal.  It can also be triggered upon the change of value of an input pin, and by the signal going to logic level low.  Since external interrupts on pins 2 & 3 of the ATmega328 have different interrupt vectors (addresses where the routines related to these interrupts are stored), such interrupts on these pins are distinguishable from each other, and also distinguishable from pin change interrupts you might be listening for on those pins as well.  External interrupts also have a higher priority compared to pin change interrupts, so they will be processed first.

pin change interrupt happens when the value of a pin suddenly changes state from low to high or vice versa.  The interrupt does not tell you exactly what the new value is (and the value is subject to change again by the time the interrupt can be processed), and as you will read below, pin change interrupts tend to share just a few vectors, so you might need a way to reconcile exactly who caused the interrupt.

Since my application only really needs to know about pin changes, especially since there's a small probability these sensors might get stuck High instead of being pulled back to Low upon the end of the magnetic pulse, I can leverage any and all of the input pins for my purpose.

Nota Bene: For my particular sensor, it drives High given one magnetic polarity, Low given the other polarity, and can become Undefined in the absence of the magnetic field.  I thought the sensor would pulse on its own each time with no further action from me, but the pulses did not actually appear until I used a pullup resistor on the input pin.  This is easily achieved in Arduino-land by supplanting INPUT with INPUT_PULLUP, as such:

pinMode(p, INPUT_PULLUP);

Once this was done, I was ready to move on. However, I faced yet another problem: the documentation would lead you to believe there are only three interrupt vectors you can use across all the pins. Here's what the Arduino Playground says about the topic:

  • ISR (PCINT0_vect) pin change interrupt for D8 to D13
  • ISR (PCINT1_vect) pin change interrupt for A0 to A5
  • ISR (PCINT2_vect) pin change interrupt for D0 to D7
Unfortunately, in my application, I need to read from at least four sensors.  What can I do?

The Shining Light


The Arduino documentation suggests using libraries, but links to a very scant piece of code with little documentation.  A quick Google search for this, though, yielded me a much more up-to-date and comprehensive solution: the EnableInterrupt library.  I suspected there would be an answer in here as to how to reconcile exactly which pin fired the interrupt, and sure enough, I wasn't disappointed.  It just looked a little bit different than I expected:

// These two statements must be written in this exact order!
#define EI_ARDUINO_INTERRUPTED_PIN
#include <EnableInterrupt.h>

// It's OK to use 0
// since the library doesn't really support interrupts on pin 0
// because of the implications for serial TX/RX
volatile uint8_t pinChangeIndex = 0;

void interruptFunction() {
  pinChangeIndex = arduinoInterruptedPin;
}

void setup() {
  // put this in a loop, perhaps, to initialize more than just pin "p"
  pinMode(p, INPUT_PULLUP);
  // ...
}

Simple, right?

It turns out that the ATmega has an internal register storing the index of exactly what pin toggled the interrupt, and this library exposes that for your consumption.  Now in your loop() function, all you need to do is branch off (i.e. write an if statement utilizing) the pinChangeIndex variable, and you don't have to process any of the application logic in the interrupt at all.  If you want to listen for multiple devices at once, it's possible to replace the uint8_t with a bool[] array and then replace the interrupt function's contents with pinChanged[arduinoInterruptedPin] = true

Incidentally, the volatile keyword in this context is used as a helper to the compiler so that it doesn't assume any code dealing with use of the pinChangeIndex variable is anywhere near where it might get changed.  This way, the variable's value will always be copied into one of the microcontroller's registers right before any comparison or operation is done on it, such as branching to particular spots depending on the variable's value.  The register will never contain an old copy of what the variable contained a long time ago.

May your project dreams be more attainable by unlocking cheaper microcontrollers to handle many I/O devices!

Comments

Popular posts from this blog

Making a ROM hack of an old arcade game

Start Azure Pipeline from another pipeline with ADO CLI & PowerShell

I/O 2021: New Decade, New Frontiers