Computing stuff tied to the physical world

We interrupt this program

Now for something a bit different: let’s look at how a µC handles hardware interrupts.

The concept of interrupts is quite simple: for a specific, pre-configured, hardware event – such as a byte coming in over the serial port or a change on an input pin – the µC hardware can trigger a call to an interrupt function – no matter what code is currently running. Each interrupt type can call a different function, coded in C/C++ just as everything else.

So in a way, an interrupt is like a function call, triggered by a hardware event.

This is useful for two reasons:

  • we don’t have to constantly check in our own code whether there is data (or risk the chance of missing such an event if data comes in at a high rate)
  • we can put the µC into a very low-power mode, and still wake up and respond when there is some work to do

The latter is what allows a µC to become ultra low-power. Even if we don’t have interrupts for everything that could happen, we can run a hardware-controlled timer to generate such interrupts and periodically wake up the µC. The LPC8xx series has a low power timer with a resolution as low as 100 µs, i.e. we can specify when we’d like it to be woken up again, as often as 10,000 times per second if need be. That timer is not overly accurate, but still.

There are reserved fixed names for each hardware interrupt, such as the “Wake-up Timer”. Defining and naming the function exactly as shown below is all we need to hook it up:

extern "C" void WKT_IRQHandler () {
    ...
}

For a full list of handler names for the LPC8xx series, see this code on GitHub.

Setting things up for interrupts to actually happen is more work, due to all the hardware settings and flags involved – each type of hardware peripheral has its own settings.

Here is the hoop we have to jump through just to get that timer to wake us up periodically:

LPC_SYSCON->SYSAHBCLKCTRL |= 1<<9;  // SYSCTL_CLOCK_WKT
LPC_WKT->CTRL = 1<<0;               // WKT_CTRL_CLKSEL
NVIC_EnableIRQ(WKT_IRQn);           // enable wake-up timer interrupts
LPC_SYSCON->STARTERP1 = 1<<15;      // wake up from alarm/wake timer
SCB->SCR |= 1<<2;                   // enable SLEEPDEEP mode
LPC_PMU->DPDCTRL |= (1<<2)|(1<<3);  // LPOSCEN and LPOSCDPDEN
LPC_PMU->PCON = 3;                  // enter deep power-down mode

Luckily, most of this can be placed in support libraries, so we don’t have to deal with such very low-level details all the time.

Race conditions

Unfortunately, there are some major issues in dealing with interrupts. Much of it comes from the fact that each line of C/C++ is compiled to multiple machine code instructions.

Consider this line of C, which increments the variable “n” (presumably defined as “int n”):

n = n + 1

Suppose it’s placed inside a number of interrupt routines to keep track of the total number of interrupts so far. Ok, our code is running, and numerous interrupts are triggering.

Unfortunately, the value of “n” may end up not being correct!

To explain this, let’s look at what that statement will look like, compiled to machine code:

  1. fetch the current value of N from memory
  2. add one to it
  3. put the new value back into memory

So far so good. It does exactly what we want it to. But with multiple interrupts taking place, each with this code in it, something unexpected will happen when one interrupt takes over while the another interrupt is still busy. Granted, this might not happen often, but it could.

Here’s is a possible scenario, for interrupt A being interrupted in turn by interrupt B:

  • let’s say N is 123
  • int A does steps 1 and 2, so it now wants to write 124 back to memory
  • whoops, int B just got triggered and takes control of the CPU
  • int B does steps 1, 2, and 3, and writes back 124 to memory
  • interrupt B ends, and did exactly what we expected it to
  • now int A resumes, and finishes by writing 124 to memory

Something awful happened: we just had two interrupts, but N only got incremented by 1!

Now you might think that such an occurrence is incredibly rare and can be ignored, but in reality there are many types of “race conditions” and they tend to happen quite often if not properly taken care of.

The big issue is that such problems are intermittent, non-repeatable, and therefore often incredibly hard to debug and fix. It might happen once a second, once a day, or once a year. In the rosiest case, it never does anything serious. Then again, maybe it does something really nasty, such as crashing/hanging the app, or changing a reading, or missing a byte.

Volatile

Here is another example. Suppose we had an interrupt coming in, with that “n = n + 1” increment code in it, and we were to write the following in our main application code:

int prevn = n;
while (n == prevn)
    ;

In other words: wait for the interrupt to happen and increment n.

The bad news: it probably won’t work. This code will loop forever.

The reason is that the C/C++ compiler is somewhat too clever for its own good in this case. It analyses the code, and concludes that nothing inside the loop is changing n or prevn, so the loop condition will always be true. As a result, the compiler translates this into machine language equivalent to an infinite loop: the test is considered redundant and is removed!

This is where the “volatile” keyword becomes important. We need to define “n” as:

volatile int n;

And then it works. The volatile modifier tells the compiler that the variable can change “all by itself”, i.e. it is either a hardware register, or a variable affected by interrupts.

Avoiding bugs

Implementing interrupt-driven code is easy, but getting it 100% working right often is not. The more an interrupt routine messes with memory which “normal code” also accesses, the easier it is to miss some weird boundary case which could lead to a race condition.

The way to avoid race conditions is to A) identify them, and then B) fix them by writing:

  1. disable all interrupts (or at least the ones which could interfere)
  2. run the original code, now safely protected
  3. restore the interrupt mode to what it was before

Once all “critical regions” have been properly protected, race conditions will be avoided.

So with interrupts, you have to implement things carefully. Every. Single. Line. Of. Code.

But fortunately there is a way to reduce the risk: do as little as possible in the interrupt routine, and do only the work that really cannot wait. In the case of a pin change, we could simply set a (volatile!) flag variable, and then check for it in our code when we need to.

In the case of serial data input, the usual approach is to very carefully define a (circular) buffer to hold the incoming data, until our main application code has a chance to process it. The buffering code needs extreme care to avoid every possible race condition, but once that code has been debugged and works properly, everything else can be done in normal mode.

This way, we’re tending to urgent matters in a very responsive way, without having to worry about race conditions everywhere in our application code. The risk is contained.

The moral of this story is: interrupts are incredibly useful, but they need to be developed and managed with great care. Once the interrupt logic is well-encapsulated in a library, they can make a µC-based application extremely responsive as well as ultra low-power.

[Back to article index]