Implementing Interrupt Service Routines in C++

Handling interrupts isn't a job for only C or assembly language programmers. C++ can do the job with elegance and efficiency.


February 01, 2002
URL:http://www.drdobbs.com/implementing-interrupt-service-routines/184401485

February 2002/Implementing Interrupt Service Routines in C++


Some people say that C++ has poor support for interrupt handler implementations. Others claim that ISRs (interrupt service routines) simply can’t be implemented in C++ at all, or, if they can, they’re terribly inefficient when compared to equivalent C or assembly language implementations.

The truth is that you can implement interrupt handlers in C++, and you can do so with the same low overhead imposed by C. The secret to success lies in understanding how to use C++’s language features properly, and in knowing how to organize things to take advantage of the inherent differences between the C and C++ ways of solving problems.

This article presents two different techniques for implementing interrupt handlers in C++. Each has its own set of advantages and disadvantages, but odds are that at least one of them is appropriate for whatever embedded application you are developing now.

Really.

The Problem

ISRs are functions invoked by a microprocessor in response to an internally-generated event like a divide-by-zero error, or an external stimulus like a serial port device receiving a byte of data. ISRs for most microprocessors take no arguments, return no values, and usually must exit with a different opcode than that used by regular C/C++ functions. ISRs are also sometimes called interrupt handlers.

In general, a hardware device with interrupt request capability is not tied directly to the host microprocessor. Instead, the device’s interrupt request signal is tied to an interrupt controller, a specialized peripheral that arbitrates on the device’s behalf for the processor’s attention when an interrupt request is made. Once the processor accepts the request and invokes the appropriate interrupt handler, the interrupt controller must usually be notified that the request has been acknowledged.

Each ordinary C++ member function has a compiler-generated parameter called this, which is a pointer to an object of the member function’s class type. Each ordinary member function call passes a value for this along with values for any of the function’s explicitly declared parameters. Interrupt handlers are not passed arguments by the host microprocessor, however, so ordinary C++ member functions cannot be used directly as ISRs.

C++ provides the static keyword for defining member functions that do not take a this pointer. A static member function is therefore akin to a typical C function, including its suitability for direct use as an ISR. However, the absence of the this pointer creates the expected limitation: a static member function cannot directly manipulate non-static data members of its class.

In the following examples, you will see C++ classes with ISRs implemented using static member functions. The examples differ in their techniques for managing interrupt controllers and device interrupts, and for accessing member data. Which approach to use depends on the application’s need for performance and run-time flexibility.

Unified Device and Interrupt Controller Handlers

Listing 1 shows one way to integrate interrupt controller management and device interrupt management into a single class. The example uses a static member function for an ISR and implements part of a hypothetical driver for an interrupt-driven serial device. The main function shown immediately after the class definition demonstrates its use.

In the example, the driver code communicates with the interrupt controller via the memory address 0xffffef00. A read from that location returns the status of the interrupt source, and writing ACK to that location tells the controller that we’re in the process of handling the interrupt request. When interrupt handling is complete, a write of EN tells the controller that it is safe to issue another interrupt request. This is a highly simplified model of how real interrupt controllers operate.

The interrupt_vector_table[] at the bottom of Listing 1 is a table of pointers to the microprocessor’s interrupt handlers. In most processors, this table either resides at a fixed address or at an address pointed to by a dedicated register that must be set before interrupts are enabled. (The code to do this is not shown.) In either case, the processor uses this table to direct interrupt requests to their proper handlers, which in our case is driver_c::isr.

The device_c class’s isr function manages all aspects of the associated device’s interrupt request process. It acknowledges the interrupt to the interrupt controller, writes data to the device to satisfy the interrupt request, and tells the interrupt controller to re-enable the interrupt request line. Put simply, the interrupt is considered completely “handled” when isr exits.

Static member functions like device_c::isr cannot access non-static member data, since there is no this pointer to dereference. The device_c class therefore declares most of its member data to be static, to allow isr to directly access the class’s transmit and receive buffers, indexes, and other important information.

The device_c::interrupts data member is declared non-static, to illustrate how to use per-instance member data from a static member function. The me pointer is a this-look-alike, but is of limited use in practice: an object of type device_c has so much static member data, it is dangerous to create more than one instance of it except under the most carefully controlled conditions.

ISRs typically observe different calling conventions from those used by other member or non-member functions. For example, since an ISR may interrupt almost anything, it must save scratch registers upon entry and restore those registers as it exits. In addition, most processors use a different machine instruction for returning from an interrupt than for returning from any other kind of function. Some compilers provide a form of #pragma directive to alter the calling conventions for functions. For example,

#pragma interrupt
void device_c::isr (void)
{
...
}

tells the compiler to compile device_c::isr(void) using the interrupt handler calling conventions.

When using a compiler without such a pragma, you may have to provide a short assembly language function that implements ISR calling conventions. This assembly code, in turn, invokes device_c::isr, the actual handler written in C++.

Listing 2 provides an outline for an assembly language function to use when no #pragma interrupt or equivalent is available from your compiler. The address of this code would be placed into the processor’s interrupt vector table, instead of the address of a function like device_c::isr.

Notice the unusual spelling of isr in the assembly language code. To implement overloading, C++ compilers “mangle” function and data object names during compilation. Mangling conventions vary, so the best way to find out the name output by your compiler is to look at an assembly language listing of device_c::isr.

Separating Device Driver and Interrupt Controller Management

The unified approach to device interrupt management makes widespread use of static data, including the awkward me pointer workaround for supplying a this pointer. As such, the device_c class is effectively a singleton class — it is designed to be instantiated no more than once. This approach is obviously inappropriate in any system with multiple instances of a given device. For embedded systems featuring arrays of devices of the same type, especially when connected to the host microprocessor through multiple or dissimilar interrupt controllers, a more sophisticated strategy is clearly needed.

Listing 3 shows an alternative to the unified strategy. In this approach, an interrupt handler specific to the interrupt controller hardware gets the interrupt request first, deals with the interrupt controller, and then forwards the request on to one or more device driver classes for further processing. The device driver classes then tackle the device-specific portions of the interrupt event.

Driver objects invoked by the interrupt controller handler often inherit from a common base class, to allow the interrupt controller to uniformly find and invoke them. The example code illustrates this by reimplementing the device_c driver class from the previous section as a derivative of the irq_handler_c class.

Device classes like device_c notify the interrupt controller object of their ability to handle device interrupts by invoking the interrupt controller’s register_handler member function. This function stores the address of the device handler object in the controller’s device_table[] array, from which it is invoked by irq_controller_c::isr when an interrupt request is received.

When interrupt controller management code and device management code are split into two classes, the device interrupt handlers become ordinary (instead of static) member functions because they no longer directly respond to interrupt request events. This eliminates the need for static data members, which simplifies the use of multiple instances of a device driver by getting rid of the me pointer used to access per-instance data in the original device_c implementation. You could even create and destroy device interrupt handlers at run time, in response to hardware upgrades, configuration changes, and device failures.

Device interrupt handler classes that don’t manage the host system’s interrupt controller hardware can be used in multiple host configurations without modification, because the class is not affected by changes in interrupt controllers and memory maps. As an added bonus, device interrupt handler objects can also be used in non-interrupt-driven modes by having the application or interrupt controller class periodically invoke the driver’s isr function. This is an especially useful feature during debugging.

An intelligent interrupt controller class could queue, postpone, and reschedule interrupt requests, perhaps with the assistance of the host’s operating system. It could provide spurious and runaway interrupt handling on behalf of all device handlers and could even probe interrupt lines to help a device handler determine which one its device was tied to. In a properly partitioned system, the addition of these features would not require any corresponding changes to existing device interrupt handler classes.

What about Performance?

Carefully designed C++ device and interrupt controller management classes have the same run-time performance as equivalent C interrupt handlers, because the underlying implementations are actually the same. For example, a good C++ compiler implements a static C++ member function just like a C function, and the me indirection in my unified handler C++ approach, along with the this pointer manipulation for per-instance data used elsewhere, is just like storing data in a dynamically-allocated C data structure.

The virtual function call in Listing 3 uses a few more instructions than a non-virtual function call with the same type and arguments. Unfortunately, eliminating the virtual function call makes the irq_controller_c class less flexible and more difficult to understand, because you have to change device_table[] from a table of object pointers to a table of static member function pointers.

If virtual function call overhead is a problem in your application, then it is likely that (a) you aren’t managing the processor’s interrupt state properly, or (b) your need for speed is beyond what the irq_controller_c class (and maybe even C/C++) can provide.

In a careful implementation, you may find that your C++ interrupt handling code runs faster than equivalent C code. This is because most C++ compilers understand in detail the fundamental constructs of the language and can optimize things like this pointer references with greater awareness than manual pointer dereferencing in C.

In summary, if you can get by with C for interrupt service routines then you can almost certainly get by with C++, because where it matters — at the instruction set level — you almost can’t tell the difference. Check out the assembly language produced by your C and C++ compilers if you need some reassurance.

Performance and Clarity, with No Downside

Contrary to popular opinion, C++ is an excellent language for embedded systems development, and strategies like the two shown in this article for implementing ISRs demonstrate ways to get the power and flexibility of C++ in embedded work, with the same performance as an equivalent C solution.

Proper use of C++ in embedded systems leads to interrupt handlers and applications that are fast, robust, and flexible. Interrupt handlers are often the most expensive and difficult parts of a system to develop, and C++ can really help make the job easier.

Acknowledgement

The author wishes to thank Dan Saks for his help in reviewing this article.

Bill Gatliff is an independent embedded developer and training consultant with 10 years of experience in assembly language, C, and C++. He welcomes questions and comments, and can be reached via his website at <www.billgatliff.com>.

February 2002/Implementing Interrupt Service Routines in C++/Listing 1

Listing 1: A unified device and interrupt controller management class

// a fifo implementation of some kind
class fifo_c {
public:
  fifo_c();
  void put (int c);
  int get (void);
};

// Part of a driver class for a hypothetical, memory-mapped, 
// interrupt driven serial i/o device tied to the host processor 
// through an interrupt controller chip.
class device_c {
private:
  // transmit buffer
  static fifo_c txbuf;

  // register map for the device
  enum {TR = 0, SR = 1, CR = 2};

  // the lsb of SR is the status of the transmitter
  // interrupt request signal; the lsb of CR is the
  // transmitter enable.
  enum {SR_TXI = 1, CR_TXE = 1};

  // the base memory address of the device
  volatile static char* devaddr;

  // commands for the interrupt controller hardware
  enum {ACK=0, EN=1, DIS=2};

  // the base memory address of the interrupt controller
  volatile static char* irqaddr;

  // a this-lookalike, for nonstatic data
  static device_c* me;

  // nonstatic interrupt counter
  int interrupts;

public:

  // the device interrupt handler
  static void isr (void);

  device_c (char* devioaddr, char* irqioaddr)
  {
    // Save our "this" pointer.
    me = this;

    // Save the device's hardware address.
    devaddr = devioaddr;

    // Save the address of the interrupt controller.
    irqaddr = irqioaddr;
  };

  void write (char c)
  {
    // Stuff the byte into the transmit buffer.
    txbuf.put(c);

    // Enable the transmitter.  The transmit interrupt
    // handler will subsequently grab the byte out
    // of the buffer and stuff it to the hardware.
    devaddr[CR] |= CR_TXE;
  }
};

#pragma interrupt
void device_c::isr (void)
{
  int c;

  // Count the interrupt.  The interrupt count
  // is non-static, thus we have to use our copied
  // "this" pointer to find it.
  me->interrupts++;

  // Acknowledge the interrupt request
  // to the interrupt controller chip.
  *irqaddr = ACK;

  // Figure out what the interrupt request is, and
  // service it.  We may have multiple requests
  // pending, so try to service them all.
  while (devaddr[SR]) {

    if (devaddr[SR] && SR_TXI) {
      // The transmitter wants another byte,
      // give it one if we have one.
      if ((c = txbuf.get()) != -1 )
        devaddr[TR] = c;
    }

    // Handle other types of device interrupts here.
    // ...
  }

  // Re-enable interrupt requests through the controller.
  *irqaddr = EN;

  // If the "interrupt" pragma is supported, then this
  // return is an RTE instead of an RTS. If the pragma
  // isn't supported then we'll need a wrapper function;
  // see the article text for an example.
  return;
}

device_c serial1((char*)0xffff0000, (char*)0xffffef00);
extern void (*interrupt_vector_table[])();
#define IRQ_DEVICE 1

int main ( void )
{
  const char* hello = "hello, world!\n";
  const char* hellop =  hello;
  
  interrupt_vector_table[IRQ_DEVICE] = serial1.isr;
  while (*hellop) serial1.write(*hellop++);
  return 0;
}
— End of Listing —
February 2002/Implementing Interrupt Service Routines in C++/Listing 2

Listing 2: An assembly language stub for when #pragma interrupt is unsupported

device_isr:
  // save scratch registers not preserved by isr()
  push r0
  push r1
  call __isr_15device_c
  // restore scratch registers
  pop r1
  pop r0
  rte
— End of Listing —
February 2002/Implementing Interrupt Service Routines in C++/Listing 3

Listing 3: Splitting device and interrupt controller management into two classes

// A uniform framework for device interrupt handlers.
class irq_handler_c {
public: virtual void isr (void) {return;}
};

// Code to manage a hypothetical interrupt
// controller chip, through which a device
// of type device_c and others is connected.
class irq_controller_c {
private:
  // Table of device interrupt handlers.
  static irq_handler_c* device_table[];
  
  // Memory address of the controller chip.
  volatile static char* irqaddr;

  // Commands recognized by the controller chip.
  enum {ACK = 0, EN = 1, DIS = 2};

public:

  // The controller's interrupt handler.  The microprocessor gives
  // control to this function whenever any device connected to the
  // controller issues an interrupt request.
  static void isr (void);

  irq_controller_c (char* irqioaddr) {irqaddr = irqioaddr;};

  // When a device handler wants interrupt requests for a device,
  // it registers with us using this function.
  void register_handler (int irq, irq_handler_c* handler)
  {
    device_table[irq] = handler;
    return;
  }
};

#pragma interrupt
void irq_controller_c::isr (void)
{
  int irq;

  // Tell the controller we're starting interrupt processing.
  *irqaddr = ACK;

  // determine which device is reqesting service
  irq = *irqaddr;

  // Invoke the interrupt handler for the requesting device.
  device_table[irq]->isr(irq);

  // We're done, reenable the interrupt request line.
  *irqaddr = EN;

  return;
}

// The interrupt controller chip has 16 request lines.
irq_handler_c* irq_controller_c::device_table[16];

// The reimplemented device_c class, with interrupt
// controller management-related code removed since
// that responsibility now goes to irq_controller_c.
class device_c : public irq_handler_c {
private:
  fifo_c txbuf;
  enum {TR = 0, SR = 1, CR = 2};
  enum {SR_TXI = 1, CR_TXE = 1};
  volatile static char* devaddr;
  int interrupts;

public:
  void isr (int irq);
  device_c (char* devioaddr) {devaddr = devioaddr;}
  void write (char c) {txbuf.put(c); devaddr[CR] |= CR_TXE;}
};

void device_c::isr (void)
{
  int c;

  interrupts++;
  while (devaddr[SR]) {
    if (devaddr[SR] && SR_TXI) {
      if ((c = txbuf.get()) != -1 )
        devaddr[TR] = c;
    }
    // ...
  }
  return;
}

extern void (*interrupt_vector_table[])();

// The irq controller chip is tied to entry 100
// of the host microprocessor's interrupt vector table.
// The controller chip is located at address 0xfffffe00.
#define IRQ_CONTROLLER 100
irq_controller_c irqctl((char*)0xfffffe00);

// The serial device is tied to pin 1 of the interrupt controller,
// and is located at address 0xffff0000.
#define IRQ_DEVICE 1
device_c serial1((char*)0xffff0000);

int main ( void )
{
  const char* hello = "hello, world!\n";
  const char* hellop =  hello;
  
  irqctl.register_handler(IRQ_DEVICE, &serial1);
  interrupt_vector_table[IRQ_CONTROLLER] = irqctl.isr;

  while (*hellop) serial1.write(*hellop++);
  return 0;
}
— End of Listing —

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.