Modeling interrupt vectors

September 07, 2006

Dan_Saks-September 07, 2006

Just as you can often treat device registers as a memory-mapped struct, you can treat an interrupt vector as a memory-mapped array.

In my last column, I suggested that you use casts sparingly and with caution.1 One of the places where programmers overuse casts is in setting up interrupt vectors. As is often the case with low-level programming, you may not be able to eliminate every cast as you set up an interrupt vector, but you can eliminate most.

Interrupts and interrupt types
When using interrupts, the application and the device do their own things until the device is ready to interact, at which point the device notifies the host processor by issuing an interrupt request. For example, a serial port might issue an interrupt request when a new character arrives. The processor responds to the request by transferring control to an interrupt handler or interrupt service routine (ISR), code that communicates closely with the device.

Some devices may be capable of issuing interrupts; others may not. For example, on my ARM Evaluator-7T single-board computer, the push button, serial ports, and timers can request interrupts, but the slide switches and LEDs cannot.

Some processors associate each interrupting device with a different type of interrupt. In that case, each interrupting device can be handled by its own ISR. Other machines make no distinction among interrupt types and route all device interrupts to a common ISR. In that case, the lone ISR has to do some detective work to determine which device requested the interrupt so it can service that device.

Some processors take an intermediate approach: they support multiple interrupt types, but not enough types to give each device its own ISR. They may also associate interrupt types with other events, such as a system reset or trap. For example, the Evaluator-7T's processor has seven interrupt types: two for device interrupts and five for other events. The two device interrupt types are (1) "plain" interrupt request (IRQ) and (2) "fast" interrupt request (FIQ). Each interrupt-capable device on the Evaluator-7T can be programmed to issue either an IRQ- or FIQ-type interrupt, or to issue no interrupt at all. Interrupt vectors

A processor typically maps each interrupt type to a corresponding pointer in low memory. The collection of pointers for all the interrupt types is an interrupt vector. Each pointer in the vector points to the ISR for the corresponding interrupt type.

For example, the interrupt vector on the Evaluator-7T occupies eight 32-bit words starting at address 0x20. Each word corresponds to one of the interrupt types, as shown in Table 1.

The program must fill in the vector before the first interrupt request occurs. Thereafter, when a device issues an interrupt request, the processor invokes the ISR whose address resides in the interrupt vector element corresponding to the requested interrupt type.

In this example, the address of the IRQ interrupt handler should be stored at address 0x38. A good time to do this is during program startup.

ISR calling conventions
Applications never call ISRs directly--ISRs run asynchronously in response to interrupt requests. To an application, each ISR appears to just start up on its own. Since no one calls the handler, no one can pass it arguments or accept its return value. Therefore, a handler in C or C++ must be a function with no parameters and a void return type. This implies that an interrupt handler in Standard C++ can't be an ordinary class member function, as in:

class timer
    {
    ...
    void handler(); // won't work
    ...
    };
because members have an implicitly declared parameter named this. However, a handler could be a static class member function, because a static member function doesn't have an implicitly declared parameter.

On most platforms, you can't even implement a handler as an ordinary C or C++ function declared as:

void handler();

because a handler doesn't observe the calling conventions of an ordinary function. Since a handler can interrupt the application almost anywhere during execution, it must be more cautious than other functions about disturbing the current processor state, particularly the processor registers. Thus, the entry code for a handler typically saves registers that ordinary functions don't. In addition, the machine code for returning from an ISR is usually different from the code for returning from a function call.

Since an ISR is unlike an ordinary function, you must either declare it using platform-specific language extensions, or write at least a portion of it in assembly language. For example, some C/C++ compilers for Intel x86 processors provide a keyword interrupt or _ _interrupt for declaring interrupt handlers as in:2

void __interrupt handler(void);

For example, the Keil's CARM compiler for ARM processors provides a non-standard keyword __irq for declaring ISRs for IRQ type interrupts, as in:3

void handler(void) __irq;

Without such extensions, you must write ISRs in another language, most likely assembler. You could write an entire ISR in assembler, but unless you're a masochist, you won't want to. Rather than completely handle the interrupt, the assembler routine can just save registers upon entry, call a C or C++ function to do the real work, restore registers, and exit via the appropriate return-from-interrupt instruction.

Filling interrupt vector elements
Some operating systems provide one or more functions for placing handler addresses into the interrupt vector. A call to such a function looks something like:

set_interrupt_vector(offset, handler);

which stores the address of handler into the interrupt vector element at the specified offset.

If no such function is available, assigning a handler to an interrupt vector element is usually still pretty easy, but I've seen programmers do it more sloppily than necessary. Here's a quick and very dirty way to place the address of the function IRQ_handler into the interrupt vector element corresponding to the IRQ interrupt on the ARM Evaluator-7T:

*(void **)0x38 = (void *)IRQ_handler;

Although this technique works on this and other platforms, neither the C nor C++ standards guarantees that it will work. When the compiler sees this expression, it treats function name IRQ_handler as if it had type "pointer to function." Casting IRQ_handler to type void * has undefined behavior because a pointer to void should point only to data, not to functions. Rather than use void *, you should use a "pointer to function" type.

Recall that a handler is a function with an empty parameter list and a void return type. A typical declaration for a handler looks something like:

void IRQ_handler(void);

possibly with an additional platform-specific keyword or two. I recommend defining a typedef for a pointer that can point to a handler, such as:

typedef void (*pointer_to_ISR)(void);

Then you can place the address of the IRQ_handler function into the interrupt vector using:

*(pointer_to_ISR *)0x38 = IRQ_handler;

which avoids undefined behavior.

As I explained in my last column, in C++ you should use a new-style cast, as in:1

*reinterpret_cast<pointer_to_ISR *>(0x38)
    = IRQ_handler;

Still, casts involving arbitrary addresses are cryptic at best and likely to be error-prone. Instead of writing one of these expressions for each interrupt type and interrupt vector element, you can treat the entire interrupt vector as a single memory-mapped object and apply the memory mapping techniques I presented last year.4 This method enables you to fill in the interrupt vector in a straightforward manner, using only one cast.

The interrupt vector is just a memory-mapped array of pointer_to_ISR at a particular base address. On the Evaluator-7T, that address is 0x20. You can define a pointer to the base of that array as a macro:

#define interrupt_vector \
    ((pointer_to_ISR *)0x20)

or as a constant object:

pointer_to_ISR *const interrupt_vector
    = (pointer_to_ISR *)0x20;

In C++, those casts should be reinterpret_casts.

You can use enumeration constants as symbolic names for the interrupt types:

enum interrupt_type
    {
    it_reset,
    it_undefined_instruction,
    it_SWI,
    it_prefetch_abort,
    it_data_abort,
    it_reserved,
    it_IRQ,
    it_FIQ
    };

Then you can fill in each interrupt vector element using a simple, easy-to-read expression, such as:

interrupt_vector[it_IRQ] = IRQ_handler;

If you do these assignments at system startup while interrupts are disabled, this is probably all you need to do to fill in the vector. However, if you need to install new handlers while the system is up and running, you may need to disable interrupts briefly while doing these assignments.

I hope to address this and other interrupt-handling issues in future columns.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at dsaks@wittenberg.edu. For more information about Dan click here .

Endnotes:
1. Saks, Dan. "Cast with caution," Embedded Systems Design, July 2006, p. 15.
Back

2. Rusch, Daniel G. "Encapsulating ISRs in C++," Embedded Systems Programming, February 1998, p. 72. Article is not available online. For a copy contact reprints.
Back

3. CARM User's Guide. Keil, 2006. Available at www.keil.com/support/man/docs/ca/ca_le_irq.htm.
Back

4. Saks, Dan. "More Ways to Map Memory," Embedded Systems Programming, January 2005, p. 7.
Back

Loading comments...

Most Commented

Parts Search Datasheets.com

KNOWLEDGE CENTER