Interrupts in C++ - Embedded.com

Interrupts in C++

An ideal C++ device driver would be a class containing, among other things, the ISR as a member function. But this is harder to achieve than many C programmers assume.

One of the goals of a recent project was to evaluate the effectiveness of C++ in writing low-level device drivers. With a push to reduce time to market, we were given a budget large enough to order some nice object modeling tools.

Everyone in the team was experienced in using C in embedded systems. We also had a smattering of C++ experience in non-embedded applications. This was simply going to be a matter of C++'ing our old C tricks, right?

We designed and coded some nifty Timer and UART drivers in C++. Data encapsulation worked great to hide the hardware details from the rest of the software. Transmit and receive buffers were nicely tucked away as private data that couldn't be touched by the rest of the program. This C++ stuff was working great!

We ran into a snag, however, when we tried to write interrupt handlers.

Most every embedded application we've worked on needed interrupts of one sort or another. Interrupts have traditionally been the software component closest to hardware and, therefore, had the least amount of abstraction and sophistication. Little did we realize that the old C ways for handling interrupts could not be C++'ified in a straightforward manner.

This article describes a design that we eventually settled on for our project. We also present some performance measurements that we took to ensure that any C++ overhead was bearable.

Interrupt handling in C

There are several ways of handling interrupts using C. Some RTOSes provide complete interrupt handling that a C application can hook into. If that support is not present, you can write your own interrupt handlers without much difficulty. There are several ways to accomplish this.

Jump tables

One way of handling interrupts is by using jump tables. These are usually written in assembly language. After the interrupt routine determines the number of the interrupt, it jumps to some multiple of the interrupt number in the jump table, where a call statement will call the interrupt service routine. An example of such a table, in ARM7 assembly, is shown in Listing 1.

Listing 1: An example jump table for ARM7

.
(interrupt number determined and in r0)
Interrupt_Handler:
ldr PC, [PC, r0, LSL #2] ; sets PC to current PC plus
; interrupt number * 4
nop
.data.w Interrupt1 ; Beginning of jump table
.data.w Interrupt2
.data.w Interrupt3
.data.w Interrupt4

Interrupt1 through Interrupt4 are the names of interrupt service routines. Each interrupt service routine must end with a return from interrupt instruction.

One problem with this method is that it is time consuming to add additional interrupts. The table needs to be updated and new interrupt service routines added. Also, if changes need to be made, each developer has to touch this file, increasing the possibility of error. That said, this is a popular and fairly straightforward method.

Interrupt vector array

In the previous method, adding new interrupts involved modifying the core interrupt service routine functions and adding functionality. One way to make this process easier is to employ interrupt vector tables, using C's ability to take a pointer to a function. The developer writes the interrupt service routine (ISR) in C. A pointer to the ISR is then stored in a global ISR pointer array, indexed by the interrupt number. When an interrupt occurs, the interrupt handler indirectly calls the ISR by calling the function pointer stored in the array at element “interrupt number.” A Register() function can be written to store the function pointer in the array based on the interrupt number, as shown in Listing 2.

When the interrupt occurs, InterruptHandler is called and the interrupt number is determined, as shown in Listing 2.

Listing 2 A Register() function written to store the function pointer in the array based on the interrupt number

typedef void (*FUNCPTR)(int);

FUNCPTR FuncPtrArray[MAX_INTERRUPTS];

void Register(int interrupt_number, FUNCPTR func_ptr)
{
    FuncPtrArray[interrupt_number] = func_ptr;
}

void InterruptHandler(void)
{
    int interrupt_number;

    // Determine interrupt number and store in interrupt_number
    …

    // Call the ISR
    FuncPtrArray[interrupt_number]();
}

void DeviceISR(int interrupt_number)
{
    // Handle the interrupt.}

main ()
{
    …
    Register(DEVICE_INT_NO, DeviceISR);
    …
}

One issue with this method is that information and code specific to the device's interrupt handling is outside the device driver. This violates the goal of data hiding. And, as shown, it can only be done in C! You can't take the address of a non-static member function and store it in an ordinary function pointer.

For example, in C++, if you have a class Timer , with a non-static memeber function ISR() , and an instantiation of MyTimer , you cannot take the address of MyTimer.ISR() . Because C++ functions are bound to instance data, a pointer to an instance function is a lot more complicated than a simple C pointer to function.

This was the fundamental roadblock we ran into.

Our solution

At this point, we stepped back, and tried to see if we could design our way out of this mess. By the end of our design, we ended up using virtual functions, the this pointer, and friend classes.

Looking at the problem from an object-oriented view, a device “has an” interrupt. Therefore, it makes sense to make an ISR class. Each device has an ISR, but all of the ISRs have several common methods and attributes.

This led to us creating an Interrupt base class. A device interrupt class is written for each device that inherits from the Interrupt base class. Each device interrupt class has an ISR. This method is what is called when the interrupt occurs.

So how does the correct ISR get called when a particular hardware interrupt occurs? Four C++ concepts are used: static member functions, virtual functions, this pointers, and friend classes.

Static member functions can be called just like normal C routines. This is important because it provides the bridge from assembly to C++. An assembly routine (or vector) can call a static member function directly since a static member has no instance data associated with it-it is static across all instances of the class.

A virtual function is defined in a base class, and is implemented differently in each inherited class. Each different class that inherits from the base class can implement the virtual method in a unique way. The ISR method is a good candidate for a virtual method, as each device needs to have its own ISR. Every derived class will have a function called ' , but their implementations can be unique.

Every non-static member function has an implied parameter. Every non-static member function of a class has an implied parameter called this . However, static member functions do not have a this parameter. In order for our static interrupt handler to manipulate instance data, we must have a stand-in. The this pointer to a given device interrupt class is stored in a static array in the Interrupt base class when Register is called. When a given interrupt occurs, the interrupt handler indexes into the array using the interrupt number, and calls the ISR using the this pointer from the array:

ISRVectorTable[interrupt_number]
->ISR();

This looks a whole lot like the C call of FuncPtrArray[interrupt_number]() , but with ->ISR() on the end of it.

Friend classes are used to maintain the concept that a device “has” an interrupt, rather than “is” an interrupt. Each of the Interrupt_N() functions needs to call ISR() in one of its derived classes. If we forced all devices (UARTs, timers, and such) to derive from the Interrupt base class, we wouldn't need friend classes. But we strongly feel that a UART “has an” interrupt, which means there should be a separate interrupt class hierarchy. To let this separate interrupt class manipulate the private members of the device class, it needs to be a “friend” of the device. For example, a UARTTransmitInterrupt class will be a friend of UART, allowing the interrupt class to get a hold of the UART's private transmit queue. A diagram of this solution is shown in Figure 1.


Figure 1

The processor's vector table gets initialized to point to the static functions in the Interrupt base class. When an interrupt occurs, the processor uses the vector table to jump to the static member function (say Interrupt_0 ). Inside of Interrupt_0 , it uses the registry table to invoke the properly registered ISR() function. Remember, the static functions act as a bridge between assembly and C++ objects. Once the code has jumped to the proper ISR function, the object can manipulate the private data of its parent.

Sample code for our solution is shown in Listing 3. This example shows a timer. When the timer interrupt fires, a private data member in the Timer class is incremented by the interrupt handler.

Listing 3 Sample code

class Interrupt{public:
    Interrupt(void);
    static void Register(int interrupt_numberber, Interrupt* intThisPtr);
    // wrapper functions to ISR()
    static void Interrupt_0(void);
    static void Interrupt_1(void);
    static void Interrupt_2(void);
    /* etc.*/
    virtual void ISR(void) = 0;

private:
    static Interrupt* ISRVectorTable[MAX_INTERRUPTS];
    };

void Interrupt::Interrupt_0(void)
{
    ISRVectorTable[0]->ISR();
}

void Interrupt::Interrupt_1(void)
{
ISRVectorTable[1]->ISR();
}

/* etc. */

void Interrupt::Register(int interrupt_number, Interrupt* intThisPtr)
{
    ISRVectorTable[interrupt_number] = intThisPtr;
}

class Timer; // forward declaration

class TimerInterrupt : public Interrupt
{
public:
    TimerInterrupt(Timer* ownerptr);
    virtual void ISR(void);
private:
    Timer* InterruptOwnerPtr;
};

class Timer
{
friend class TimerInterrupt;

public:
    Timer(void);
    int GetCount(void) { return Count; }
private:
    TimerInterrupt* InterruptPtr;
    int Count;
};

Some comments on the code

The Timer constructor instantiates a new TimerInterrupt object, and passes in its this pointer. The TimerInterrupt constructor sets its private data member InterruptOwnerPtr to this . The TimerInterrupt object can then access the correct Timer instance data by using this data member, that is InterrruptOwnerPtr->Count . This is possible because the Timer-Interrupt class is made a friend of the Timer:

Timer::Timer(void)
{
    InterruptPtr = new
        TimerInterrupt(this);
}

When a device interrupt object is instantiated, the Register function must be called to let the Interrupt base class know that there is an appropriate ISR function for the given interrupt.

TimerInterrupt::
    TimerInterrupt(Timer* owner)
{
    InterruptOwnerPtr = owner;    
    // Allows interrupt to access owner's data
    Interrupt::Register(TIMER_INTERRUPT_NUMBER, this);
}

Don't be confused by all those this pointers flying around. Remember Figure 1: a TimerInterrupt is derived from Interrupt , and therefore must register its this with the base class. You can also see from Figure 1 that a Timer “has a” TimerInterrupt , and therefore must give the Timer-Interrupt instance a pointer to itself.

In this implementation, the device class or device interrupt class is responsible for clearing the interrupt source.

Complicating issues

One thought that occurred to us is that a one-to-one correspondence does not exist between interrupts and physical objects. Although a timer instance might have a single interrupt, a UART might have several: one for transmit, one for receive, and one for modem status line. Actually, the system we were using had an external DUART chip where each individual UART had three possible interrupt sources, for a total of six possible interrupt sources on a single chip; however, this chip was allocated just a single interrupt line running to the processor.

So not only did we have a situation with several physical interrupts needing to manipulate a single object, but we also had a single physical interrupt that needed to be handled in several different objects.

We could also see the handwriting on the wall, near the end of the project, where someone would decide that we need just one more interrupt, though no spare interrupt lines remain. Eventually we'll be able to cajole the hardware folks into sharing the new interrupt with an existing one. So not only will we have to deal with shared interrupts for the same device, but in the future we can foresee sharing interrupts between several vastly different devices.

Our solution for the many-to-one problem is easy: it's okay for an object to have several interrupts. Just as a UART class might have two queues (one for transmit and one for receive), it would be perfectly valid for the object to have two interrupts: one for transmit and one for receive.

Admittedly, the code for the two interrupts will need to be different. Actually, two diiferent interrupt classes will be necessary, a UARTXmit_InterruptClass and UARTRecv_InterruptClass . The UART driver declarations will look something like Listing 4.

Listing 4 UART driver declarations

class UARTXmit_Interrupt : public Interrupt
{
private:
    UART *InterruptOwnerPtr;
public:
    UARTXmit_Interrupt(UART *ownerptr);
    virtual void ISR(void);
};

class UARTRecv_Interrupt : public Interrupt
{
private:
    UART *InterruptOwnerPtr;
public:
    UARTRecv_Interrupt(UART *ownerptr);
    virtual void ISR(void);
};

class UART
{
friend class UARTXmit_Interrupt;
friend class UARTRecv_Interrupt;

private:
    UARTXmit_Interrupt *XmitInterruptPtr;
    UARTRecv_Interrupt *RecvInterruptPtr;

public:
    UART();};

As before, the interrupt instances need pointers back to the UART instance; I call these “owners.” The owner is, again, set in the constructor. The UART class has pointers to two interrupt handlers, which again seems logical from a hardware standpoint-the UART physically has two interrupt sources, therefore the UART driver should have two interrupt handlers.

Things get a little more complicated with the DUART, however. Remember, the problem with the DUART is that although it has several possible interrupt sources, these are all tied to a single hardware line.

Let's start analyzing this problem by starting where the UART left off. Let's assume that each UART instance of the DUART has a transmit and receive interrupt class. We can have a transmit interrupt object and a receive interrupt object that manipulate the data structures of their owners. The only problem is deciding how to route the function call from a single interrupt vector into a call to the appropriate handler.

We know from the DUART data sheet that there is a register that we can interrogate to see what the exact interrupt source is. By reading this register and doing some bit manipulation, we'll know which interrupt handler to call. One possibility was to add special logic inside of the Interrupt base class that would interrogate this secondary register.

We didn't like this for several reasons. First of all, the Interrupt base class would have to know some intimate details about the DUART registers. Second, how do we register these shared interrupts that exist on a single interrupt line? If there were eight physical interrupt lines, we would expect that there would be just eight registered functions. The concept of the shared interrupt lines just doesn't fit very well with the Interrupt manager base class.

It then occurred to us that the DUART interrupt identification register was very similar to the CPU's interrupt identification register. Why can't we have two interrupt managers? We could have the base interrupt manager that handles the eight hardware lines that come into the CPU. Derived from this base class, we could have a class that deals just with the DUART interrupt register. From this DUART interrupt class, we can derive the separate transmit and receive interrupts.

So, possibly, we'll have a class called Shared_Interrupt , which is derived directly from the base Interrupt class. The separate transmit and receive interrupts will derive and register themselves with this shared interrupt manager, much in the same way as the Shared_Interrupt will register itself with Interrupt.

Funny how the software seems to mirror the hardware.

You must be careful with one detail from this scheme. Although you don't need to instantiate the Interrupt class itself (only its derived classes), you do have to instantiate the Shared_Interrupt class. For the Interrupt class, all of its data and functions are static to the class. They exist without instantiation. There is no direct need for the Interrupt class to be instantiated.

The Shared_Interrupt , however, does need to be instantiated. Remember, the Shared_Interrupt has an ISR function that needs to be called when an interrupt occurs. To call that ISR function, it needs a this pointer that is registered with the Interrupt class. It is very important that the Shared_Interrupt have its very own instantiation.

If you don't instantiate Shared_Interrupt , and instead you only instantiate one of its derivatives, the this pointer that gets registered to the Interrupt class points to the derivative, not to the Shared_Interrupt . When an interrupt occurs, the Shared_Interrupt ISR function is bypassed, and the derivative's ISR function is called instead. So the very first instantiated derivative receives all of the shared interrupt events. The code that determines which of the shared interrupts occurred gets completely skipped over, and all shared interrupts jump to the derivative's ISR.

This can be demonstrated in the example code by commenting out the Shared_Interrupt MySharedInterrupt; declaration in main. With the code left in, each DUART interrupt handler gets called once. With it commented out, the very first DUART interrupt handler gets called four times.

Some code snippets are shown in Listing 5. Lets assume that the DUART's interrupt register is found at INT_REGISTER , and that there are four possible sources of interrupts: transmit0, receive0, transmit1, and receive1 , and these are found in bits 0 to 3 respectively. The rest of this register is 0.

LISTING 5 Handling shared interrupts

class Shared_Interrupt : public Interrupt
{
private:
    static Shared_Interrupt* ISRVectorTable[MAX_DUART_INTERRUPTS];
    public:
    Shared_Interrupt();
    static void Register(int interrupt_number, Shared_Interrupt* intThisPtr);
    virtual void ISR(void);
};

class DUARTXmit_Interrupt : public Shared_Interrupt
{
private:
    DUART *InterruptOwnerPtr;
public:
    DUARTXmit_Interrupt(DUART *ownerptr, int duart_number);
    virtual void ISR(void);
};
class DUARTRecv_Interrupt : public Shared_Interrupt
{
private:
    DUART *InterruptOwnerPtr;
public:
    DUARTRecv_Interrupt(DUART *ownerptr, int duart_number);
    virtual void ISR(void);
};

void Shared_Interrupt::Register(int interrupt_number, Shared_Interrupt*
intThisPtr)
{
    int i;
    
    for (i = 0; i < MAX_DUART_INTERRUPTS; i++)
    {
        if ((1 << i) == interrupt_number)
        {
            ISRVectorTable[i] = intThisPtr;
        }
    }
}

void Shared_Interrupt::ISR(int interrupt_number)
{
        int i;

        // cycle through all possible interrupts
        for (i = 0; i < MAX_DUART_INTERRUPTS; i++)
        {
            // check bit mask in “hardware register”

            if ((1 << i) & DUARTInterruptIDRegister)
            {
                // make sure there is a registered handler
                if (ISRVectorTable[i] != NULL)
            {
                ISRVectorTable[i]->ISR(interrupt_number);
            }
            else
            {
                printf (” Error! Unregistered shared interrupt %dnn”,
                interrupt_number);
            }
        }
    }
}

DUARTXmit_Interrupt::DUARTXmit_Interrupt (DUART *ownerptr, int duart_number)
{
    InterruptOwnerPtr = ownerptr;

    switch (duart_number)
    {
        case 1:
            Shared_Interrupt::Register(DUART1_XMIT_ID, this);
            break;
        case 2:
            Shared_Interrupt::Register(DUART2_XMIT_ID, this);
            break;
}

It's interesting to note that the DUART_XmitInterrupt and DUART_RecvInterrupt need to be derived from the class Shared_Interrupt , not from Interrupt itself. It is also interesting to note which class knows about what: the Shared_Interrupt class really knows nothing about transmit interrupts or receive interrupts, and it doesn't know that there are two separate logical UARTs. It simply knows that it needs to check a register and maintain and use a vector table of its own. It really doesn't know or care about anything else in the DUART, except that single register.

Similarly, the DUART_XmitInterrupt and DUART_RecvInterrupt hardly notice that they are shared interrupts. The only difference between these and the UART versions is that they are derived from Shared_Interrupt , rather than from Interrupt itself. Beyond that, there is no place in the code where they even admit that they are shared interrupts. They have no knowledge of each other; they only care about manipulating their own hardware transmit and receive registers, and they know that they need to register themselves with their base class. Moving from shared to separate interrupts is simply a matter of which class you derive from and which Register function you call.

This division of knowledge is much more striking for the end of the project-the we-need-another-interrupt stage. Say we are well into our project, and we find out that we need to put in some overtravel limit switches, and that marketing has requested that the widget we are working on also has a time-of-day clock with a settable alarm, and, oh yeah, a USB interface. Of course, just a single interrupt line is left open. The hardware folks tell you that they can OR the interrupt request lines together, and they can also trap the interrupts in a latch.

By using the scheme outlined here, the new interrupt manager needs to know nothing about USB, motor travel limits, or RTC chips. Its focus is that one register and its vector table. Similarly, the USB driver doesn't need to know it is being shared, nor do you have the possibility that the RTC clock code will muck around with the USB driver.

Performance concerns

There is a risk that these extra layers of abstraction could greatly increase interrupt latency. So we decided to test exactly how long things took to execute, comparing C to C++ implementations. Of course we found some performance penalties when using C++. However, there were some pleasant surprises along the way.

We used a dual-channel oscilloscope for our timing measurements. One channel was set up to watch the incoming interrupt line, a second channel was tied to an I/O line that was triggered from inside the interrupt handler. By measuring the time difference between these two events, we could determine and compare the C and C++ overhead. A basic for (i = 0; i < 10; i++); was also run as a benchmark.

The results were as follows:
for() loop        5s
C             7s
C++            10s
C shared        18s
C++ shared    31s

Although percentage-wise, the C++ calling overhead was 50% more than the C overhead, calling overhead is just part of the overall interrupt time. We were afraid that the C++ virtual function penalty would be much worse.

The C shared and C++ shared times were for shared interrupts where a loop determined which of the shared interrupts had actually fired. In this example, the C++ version took significantly longer than the C version.

After some investigation, we found that accessing instance variables takes longer than accessing either local, static, or global variables. At first we were surprised, but eventually it dawned on us that accessing an instance variable is like accessing a structure, and that there is probably a level of indirection in accessing an instances' variable space.

At first, we thought we could overcome this by using local variables as much as possible. At the entry to a function, set a local variable to the instance value, manipulate the local value, then, before exiting, set the instance to the local's value.

We didn't like this option for several reasons: it is not how programmers think, and we could envision easily forgetting this practice. We also thought this was fraught with danger because of the different ways to exit a routine. We'd have to make sure that instance variables were properly set at every possible exit. Finally, it created two extra assignments for every instance variable we wanted to manipulate.

We then turned on the first level of compiler optimization and the performance difference between instance and local variables improved. The following are the results with optimization on:

C            6.6s
C++            8.3s
C shared        18s
C++ shared    27s

In our application, the ability to support an evolving hardware platform “forever” (okay, just five years, but that's equivalent to forever in software, right?), under shifting scope and marketing demands, was deemed more important then saving a few microseconds here and there.

Future directions

The Shared_Interrupt class could probably be made more generic and more abstract by passing in the interrupt ID register address during construction, rather than being hard-coded. The “gotcha” where you need to instantiate the Shared_Interrupt class for it to work properly could probably be handled automatically inside the class.

Example code notes

The example code includes a sample of C-style function tables, simple C++ interrupts, and the more complicated version of C++ SharedInterrupts .

We wanted to give you a framework to experiment with, but interrupt handling details are so hardware specific that we can't offer a full assembly-to-virtual function example. Instead, our example code has a main() function that simply calls the Interrupt base class directly, simulating an assembly language vector call.

Timer, UART, and DUART classes are constructed, and an appropriate interrupt is simulated for these devices. Since the purpose of the code is to show calling sequences, and not to provide full hardware functionality, the ISR functions for the various devices simply increment counters. In real life, this counter increment would be replaced with code to reset a hardware timer, get and put characters from hardware FIFOs, and so on.

Also, for purposes of clarity, we left out required sanity-checking code such as bounds checking the vector tables, NULL pointer checking, and so on.

The advantage

We have described a design in which interrupt handling-typically done in assembly or C-can be done with an object-oriented approach using C++. This is useful when using an RTOS that does not do interrupt vectoring, and using as much C++ as possible is desired.

The greatest advantage is the ease of adding other devices that require interrupt handling by writing a simple device interrupt class, and registering this interrupt with the interrupt handler.

Alan Dorfmeyer is currently developing software on a handheld computer platform, and has also done Windows development for Syclo. He has also worked at Baxter Healthcare and other companies. Alan received a BSEE from Rose-Hulman Institute of Technology. He can be reached at .

Pat Baird is a principal engineer at Baxter Healthcare and has been programming embedded systems for six years. He will be earning his MBA soon from Marquette University. Prior to Baxter, he was the engineering manager at a small packaging company. His e-mail address is .

Return to August 2001 Table of Contents

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.