oreilly.comSafari Books Online.Conferences.


Black Box with a View, Part 2
Pages: 1, 2, 3, 4, 5, 6

Hello World, Interrupted

Modern operating systems provide concurrency mechanisms (typically processes and threads) that application developers can use in their programs. Concurrency is a very important feature. For example, an application with a GUI usually has a separate thread for interacting with the user, while other threads perform lengthy tasks in the background. This allows the application to remain responsive to the operator's inputs.

In embedded systems, concurrency is even more important. Events in the outside world (communicated to the microcontroller via switches, temperature sensors, strain gauges, and so on) often need immediate attention (such as opening a valve by activating a solenoid). Missing an event (or even being sufficiently late) can cause catastrophic failure (for example, if a car's antilock brakes failed to function).

The underlying mechanism that supports these concurrency facilities is the interrupt. Here--in outline--is how interrupts work. In response to certain events, the processor will stop what it is doing, save some state information so that it can come back to the stopped task later, and begin executing a piece of code called an interrupt service routine (ISR).

Each type of event has a different ISR associated with it. The ISR must respond to the event quickly--it cannot wait or go to sleep. After finishing, the ISR executes a return-from-interrupt instruction, which allows the processor to resume the previously suspended task. The overall state of the machine, however, may no longer be the same. For example, the operating system may schedule another thread to run as the result of the actions taken by the ISR.

What happens if you are writing an embedded program that runs directly on a microcontroller, without any operating system? The answer is that you must handle the interrupt mechanism directly and write your own ISRs.

This section illustrates how to write these routines, by replacing the crude delay loop from Example 2 with a highly accurate timer. The timer triggers an ISR. While the example does not demand such accuracy, it is often critical in embedded software. Control systems, for instance, need to sample real-world data (such as the temperature in a furnace or the position of a robotic arm) at regularly spaced intervals. You could use a timer-driven ISR routine--very similar to the following code--for this purpose.

In keeping with a layered, object-oriented approach, here is a timer driver, designed to work with the LED objects from Example 1:

/* File "LED_timer_driver.h".

   The header file for "LED_timer_driver.c", which implements an
   interrupt-based timer driver. */


/* The object reference for LED timer driver objects.  The object's
   methods and its constructor take this reference as an argument, but
   they do not actually use it.  It is only there for consistency with
   LED objects (see "LED_driver.h").  A consistent approach reduces
   programmer error. */
typedef int LED_Timer_Ref[1];

/* Constructor. */
void LED_Timer_Init(int* const ignore, struct _LED* const ledp);

/* Start the timer. */
void LED_Timer_Start(int* const ignore);

/* Stop the timer. */
void LED_Timer_Stop(int* const ignore);


Example 7a. The Timer Driver Header File

Here is the .c file that implements the timer driver.

/* File "LED_timer_driver.c"

   This file implements an interrupt-based timer driver.

   It also shows the composition technique in Object-Oriented
   Programming for microcontrollers, and the classic "Singleton" design
   pattern. */

#include <io.h>
#include <signal.h>

#define TIMER_PERIOD 100  /* The timer will have a period of 100+1. */

#include "LED_driver.h"

static struct _LED* _ledp = 0;

/* The constructor. */
void LED_Timer_Init(int* ignore, struct _LED* const ledp) {
  (void)ignore;              /* Suppress the unused argument warning. */
  /* Do not initialize twice.  This makes an LED timer object a
     Singleton. */
  if (_ledp) {  

  _ledp = ledp;              /* Save a pointer to the LED object. */            

  /* Note: TACCR0 and TACTL are registers which control Timer_A of the
     MSP430. */
  TACCR0 = 0;                /* Keep Timer_A off for now. */
  TACTL = TASSEL_1 | MC_1;   /* The MC_1 bit instructs Timer_A to
                             count up to the value of TACCR0, then
                             restart the count from 0 (this is
                             called "upmode"). */

  TACCTL0 = CCIE;            /* Enable the Timer_A interrupt in the
                             Timer_A control register (TACCTL0). */

/* Start the timer. */
void LED_Timer_Start(int* const ignore) {
  (void)ignore;              /* Suppress the unused argument warning. */
  TACCR0 = TIMER_PERIOD;     /* Set the period for Timer_A.  This also
                             causes the timer to start counting. */

/* Stop the timer. */
void LED_Timer_Stop(int* const ignore) {
  (void)ignore;              /* Suppress the unused argument warning. */
  TACCR0 = 0;                /* This stops the timer. */

/* The ISR. Note the syntax, which is an extension of the C language.
   Each C compiler that allows you to write ISRs will have slightly
   different syntax for this feature. 

   When Timer_A's count reaches the value in the TACCR0 register, it
   triggers this particular ISR. The MSP430 automatically clears the
   interrupt condition by the time the ISR returns.

   See the MSP430x1xx Family User's Guide for more details. */
interrupt(TIMERA0_VECTOR) timer (void) {
  LED_Toggle(_ledp);   /* Toggle the state of the LED on each Timer_A
                       interrupt. */

Example 7b. The timer driver itself

Note that if the calling program stops the timer, the LED may wind up in an "on" or "off" state. The LED objects from Example 1 lack a method to turn the LED off. You may implement such a method as an exercise. If you are using the development board covered in the first article, be aware that the LED is wired active low.

Example 8 is a new version of the code in Example 2. This new version uses the timer driver.

/* File "hello-world-isr.c".

   This program illustrates the composition technique in
   object-oriented microcontroller programming.  In addition, the LED
   timer driver used here includes an Interrupt Service Routine (ISR)
   to flash the LED. */

#include <io.h>
#include <signal.h>            /* Needed to handle interrupts. */ 
#include "LED_driver.h"        /* The LED device driver. */
#include "LED_timer_driver.h"  /* The LED timer driver. */

int main(void) {
  /* Create the LED and LED timer objects. 
  LED_Ref led; 
  LED_Timer_Ref timer;
  P2DIR=0xFF;  /* Configure port 2; all pins are outputs in this
               case. */

  LED_Init(led,&P2OUT,2);      /* Initialize the LED object. */

  LED_Timer_Init(timer, led);  /* Initialize the timer object. 
                               Note that the LED object is
                               one of the arguments. */

  eint();                      /* Enable interrupts (they are disabled
                               by default */

  LED_Timer_Start(timer);      /* Start the timer. */ 

  /* The "main" function has nothing left to do -- the ISR in the LED
     timer driver now flashes the LED.  The following MSP430-specific
     code puts the microcontroller into a partial sleep state.  See
     MSP430x1xx Family User's Guide for details.


  _BIS_SR(LPM0_bits + GIE);    /* Enter partial sleep ... */                                   

  for(;;);                     /* Infinite do-nothing loop. The system
                               will never execute it, unless you
                               remove the previous line of code. */ 

Example 8. Using the timer driver

Place all of these new files (along with a copy of LED_driver.h) in a separate subdirectory, change to that subdirectory, and compile with the command:

$ msp430-gcc -std=gnu89 -pedantic -W -Wall -Os -mmcu=msp430x149 \
    -o isr.elf hello-world-isr.c LED_timer_driver.c

Example 8 creates an LED object, then passes it to the timer object. The latter uses only the LED object's interface. This illustrates the technique of composition, as discussed previously in Object-Oriented Design for Microcontrollers. The timer and LED objects depend only on each other's interfaces--you can vary the implementation of either, and they will still work together.

Of course, there is a performance penalty associated with these benefits. Appendix C illustrates a highly simplified variation of this design that still retains some of the advantages.

As you can also see from Example 8, an embedded program running directly on the hardware effectively supports two types of tasks. There is one low-priority, long-running task (the main function, as well as any other functions that main calls) and several high-priority tasks of much shorter duration (implemented as ISRs). Example 8 uses the long-running task only to perform initialization; after it starts the timer, main puts the microcontroller into a partial sleep state.

What if a program calls an LED object's methods from the main task as well as from the timer ISR? Because the ISR and the main task are not synchronized, it is highly likely that this will corrupt the object's internal structures or something else on the microcontroller. You need to use critical regions in your code, if you share any resource between an ISR and the main task--just like in thread programming on an ordinary PC. The next article will illustrate how to define such critical regions.

To conclude this section, here are a few important points that you must keep in mind about ISRs:

  • ISRs are very difficult to correct. Always implement any new features without them, and then add the ISRs later.
  • ISRs must finish very quickly. You may need to carefully optimize your ISR code--sometimes even switching to assembly language in order to achieve the required performance. It is very important, however, that you not optimize prematurely. Make sure that a particular ISR is really at fault before rewriting it.
  • Beware the debugger. You cannot count on this tool to reliably find the faults in your embedded software.

The last point is particularly important. In an environment where events come in real time from external devices, stepping with the debugger (if it works at all) will yield no insights. After all, the external events will not pause just because the debugger has stopped the program.

Even if you set breakpoints, the timing of the code often changes so dramatically that the software behaves very differently than if the breakpoints were not present.

In fact, one major benefit of connected embedded systems is that they help with debugging. A network-enabled device can send messages about its internal state. Sending such messages tends to be far less disruptive to system operation than the blunt intervention of a debugger. That makes it much easier to find errors.

This article has illustrated some server-grade design patterns--within the resources of a tiny microcontroller. You can now create efficient layered architectures with object-oriented interfaces. The next article will use these techniques to create connected embedded systems--and take a decisive step toward the pervasive computing future.

Pages: 1, 2, 3, 4, 5, 6

Next Pagearrow

Sponsored by: