CMP EMBEDDED.COM

Login | Register     Welcome Guest IPS  Call for Abstracts
 

Interrupts in C++



Embedded Systems Design
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);
}

1 | 2

Rate this article: Low High
Current rating
  • .
Embedded.com Career Center
Ready to take that job and shove it?
SEARCH JOBS

Browse all jobs

SPONSOR
RECENT JOB POSTINGS




 :