Some people say that C++ has poor support for interrupt handler implementations. Others claim that ISRs (interrupt service routines) simply cant be implemented in C++ at all, or, if they can, theyre 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 devices interrupt request signal is tied to an interrupt controller, a specialized peripheral that arbitrates on the devices behalf for the processors 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 functions class type. Each ordinary member function call passes a value for this along with values for any of the functions 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 applications 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 were 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 microprocessors 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 classs isr function manages all aspects of the associated devices 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 classs 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 processors 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 controllers register_handler member function. This function stores the address of the device handler object in the controllers 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 dont manage the host systems 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 drivers 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 hosts 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 arent managing the processors 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 cant 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>.