Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Implementing Interrupt Service Routines in C++


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>.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.