Computing stuff tied to the physical world

Pin changes, levels, and edges

One category of interrupts which is very useful in embedded contexts is the “pin change” interrupt, tying external digital pin changes into the µC’s interrupt system.

Pin change interrupt hardware is vendor and chip-family specific, unlike the NVIC (Nested Vectored Interrupt Controller) which is basically the same for all ARM Cortex processors. For the LPC8xx, we have a fairly sophisticated pin change mechanism at our disposal.

The basic LPC8xx functionality supports up to 8 separate interrupts, each tied to their own selectable I/O pin. These 8 interrupts and their handlers can be used independently.

Level interrupts

One way to generate an interrupt is to define the level (“0” or “1”) which will generate the interrupt. A button tied between ground and an I/O pin could be used for this: we simply enable the internal pull-up on the pin, causing it to default to a “1” and when the button is pressed, it gets pulled down to “0”, generating the interrupt.

As a result, a specific ISR we have associated with that pin change interrupt will be started. The ISR’s are called PIN_INT0_IRQHandler() to PIN_INT7_IRQHandler().

So far so good, but then what? The moment the ISR returns, it would be called again if the button is still pressed, because this type of interrupt fires as long as the level matches.

Edge interrupts

For cases like this, it’s simpler to use another type of pin change interrupt – which triggers on a low-to-high or a high-to-low “edge” (or both). That way, each change triggers a single interrupt. Note that in the case of our simple switch, we’ll still get quite a few interrupts, due to switch bounce – but this is a mechanical issue, not an interrupt-specific one.

An example

Let’s try this out. This example on GitHub uses pin change interrupts to report changes on PIO2, which is pin 4 on the LPC810. It’s been wired up with a button, as described before:

DSC 5035

The code for this is as follows:

#include "sys.h"

volatile bool triggered;

extern "C" void PIN_INT0_IRQHandler () {
  LPC_PININT->IST = (1<<0);       // clear interrupt
  triggered = true;
}

int main () {
  tick.init(1000);
  serial.init(115200);

  printf("\n[button]\n");

  LPC_SWM->PINENABLE0 |= (3<<2);  // disable SWCLK/SWDIO
  // PIO0_2 is already an input with pull-up enabled
  LPC_SYSCTL->PINTSEL[0] = 2;     // pin 2 triggers pinint 0
  LPC_PININT->SIENF = (1<<0);     // enable falling edge
  NVIC_EnableIRQ(PININT0_IRQn);

  while (true) {
    if (triggered) {
      triggered = false;
      printf("%u\n", (unsigned) tick.millis);
    }
  }
}

It’s not a good idea to call printf() inside the ISR, hence this logic (more on that below).

Here is some sample output, pressing and releasing the button a few times:

[button]
968
1136
1589
1720
2200
2316
2316
2645
2756

Note that, despite responding only to falling edges (i.e. closing the button), some of these lines were triggered by contact bounce when releasing the button.

Another note is that for this simplest of all cases, interrupts would not even have been needed. All pin changes are latched and remembered by the hardware – with a slightly different approach, we could have checked (and then cleared) the hardware flag without risk of missing any edge events. Still, in most situations interrupts will be more useful.

Who needs levels?

You would think that edge-type pin change interrupts are all we need. Why bother with levels at all? Level interrupts come in handy when there is a bit of logic at the other end.

Let’s take the RFM69 wireless module, which has several pins to indicate some specific event has happened. The DIO0 pin, for example, can go high when the radio has received a complete packet. This pin then stays high until the packet has been read by the µC.

So what we can do, is trigger on a high level, and let the ISR read the packet. When the ISR returns, the cause of the interrupt is gone. It won’t fire again until a new packet comes in.

This will add some subtle complexity to the RF69 driver, though. We can no longer just fire off SPI bus requests at will, because now there is an ISR which (at any time!) could do the same, and mess up the SPI bus if there is currently another request going on.

Ignoring this issue for a moment, the logic for our new RF69 driver then becomes:

  • define an ISR, and make it call the RF69::receive() code when DIO0 is high
  • finish the ISR by setting a global flag to indicate that a packet is ready
  • replace calls to receive() in the app by a check on that global flag
  • once the packet has been picked up, clear the global flag
  • and to make things easier: wrap the above two steps in a new function

This approach also requires defining a global packet buffer to hold the received data, because we can obviously no longer control when a packet is goint to be read out.

The extra complexity we’ll need in the RF69 driver to make it “interrupt-safe” consists of adding some sort of locking to prevent interrupts at certain critical times. Maybe we can simply disable the new DIO0 pin-change interrupt whenever we access the bus, i.e.:

  • disable pin-change interrupt N
  • enable the SPI select
  • read/write bytes over SPI as needed
  • disable the SPI select
  • re-enable pin-change interrupt N

As you can image, this will affect many parts of the current RF69 driver. Which is why actually making these changes and testing them will be left for another time.

Now let’s get back to why this needs level interrupts and can’t be done 100% reliably with edge interrupts. Consider this scenario:

  • a packet arrives, and sets DIO0 to “1”
  • the µC registers it and needs to handle the interrupt
  • it’s a bit busy though, so it’s not calling our ISR right away
  • a second packet comes in
  • finally, our ISR gets called, does its thing, and returns
  • since there is still a packet pending, DIO0 remains “1”
  • we never get a second interrupt

The benefit of a level interrupt is that it “interlocks” with the actual cause in a more robust manner: the level stays in the triggering state as long as there is a reason to interrupt. Only after every reason to interrupt is gone will the level drop back to its idle state.

(update: the RFM69 is probably not a good example after all, as it probably can’t handle multiple packets as just described – but other hardware, such as I2C or UART chips, can)

It might seem that this way of using interrupts adds very little, since we’re still checking to find out when a packet has been received, but now through a variable instead of a hardware register. There are two important reasons why this is nevertheless very useful:

  • interrupts can wake up the µC, so it can switch to a low-power state until then
  • we can extend the ISR to buffer multiple packets, even if not used right away

For high-speed I/O (including the serial port example given earlier), additional buffering can achieve much higher data rates, since we’re no longer forced to constantly poll and extract (or send out) individual bytes – only the ISR needs to be fast enough.

Practical use

It can’t be said often enough: interrupts are tricky to get right. They can happen at the worst possible time, and Murphy’s law says they will, in due time, when least expected.

If possible, consider using the following mechanism to deal with pin-change interrupts:

  • set them up as needed, edges, levels, whatever
  • have the ISR(s) only clear the interrupt and set a global flag
  • then, in your application somewhere, periodically check for that flag

In code (this is the same as the above button example):

volatile bool triggered;

extern "C" void PININT0_IRQHandler () {
  LPC_PININT->IST = (1<<0);
  triggered = true;
}

void main () {
  ...
  if (triggered) {
    triggered = false;
    // do the real work here
  }
  ...
}

This has the following properties:

  • the ISR code remains as simple and quick as can be
  • it’ll catch every pin change, no matter how busy the µC is
  • all the real work is done in a normal state, not in interrupt mode
  • the low-level ISR and high-level app logic are cleanly separated
  • notice the (essential!) use of the volatile attribute here

There is a drawback with the above, which can be overcome with some extra coding: multiple triggers in very rapid succession may end up getting “coalesced” into one.

Pattern match engine

The LPC8xx pin-change hardware also has another mode, which does pattern matching. Without going into too much detail right now, this mode allows you define combinations of pin-change events as interrupt causes, as opposed to each pin being an individual source of pin-change interrupts. For example: only trigger an interrupt when pin 1 goes from high to low and pin 2 is “0” and pin 3 is “1”.

The LPC8xx hardware supports either simple pin changes or this pattern-match mode. Pattern-matching is in fact a superset, but setting it up is considerably more involved.

[Back to article index]