Although interrupt service routines (ISRs) are frequently written in assembler, it's easier to handle complex interrupts by writing the body of the ISR in C or some other high-level language. All you need is a little assembler and some knowledge of your compiler’s calling conventions.
Inside an interrupt
An interrupt occurs when an event from a hardware peripheral or processor exception signals the CPU. The CPU responds by diverting the normal flow of execution to an interrupt-service routine that handles the event and then returns control to the interrupted code.
The ISR is essentially a function that takes no parameters and returns no results. But, unlike a regular function, you can call an ISR at almost any time, so you must therefore take special precautions. These precautions typically take the form of special entry and exit code sequences.
- Save registers and other state information that interrupt-processing might modify to preserve the execution of the code.
- Prepare the CPU environment before executing the main body of the interrupt handler.
- Restore registers and other saved state information.
- Execute a special “return from interrupt” instruction to return to the interrupted code.
Regular compiled functions don't have these special entry and exit code sequences, so you can't call them as ISRs.
Built-in compiler support for C interrupts
Some compilers provide built-in support for interrupts using #pragma or nonstandard keywords that tell the compiler to generate the ISR entry and exit code sequences for marked functions. Here's an example:
interrupt void ISR(void)
/* interrupt processing here */
You can then safely use this function as an ISR. If your compiler provides this support, be sure to check that the generated entry and exit code is appropriate. Your run-time environment might be slightly different from the one assumed by your compiler vendor.
Assembler wrapper for C interrupt handler
If your compiler doesn't have built-in support for interrupts, you can place the body of the ISR processing in a regular C function and call it from a simple assembler wrapper. You should install the assembler wrapper as the ISR. It's responsible for executing the special entry and exit code sequences on either side of the call to the C function.
But which registers must be saved and restored? And how do you prepare to call into the high-level C environment? You can usually find this information in the compiler documentation. Look for a section on calling conventions, interfacing to assembler, or something similar. You need to find the answers to two questions:
- Which registers will a function call alter, specifically when calling a function that takes no parameters and returns no result?
- Does the C environment make any assumptions about the operating environment? This might include the state of CPU flags, the processor mode, or fixed register values.
Once you have this information, you're ready to write the assembler wrapper. Here's an example implementation for an unnamed 80386 C-compiler. The documentation for this compiler says the following:
- A function will preserve all registers except EAX, flags, and the registers used to pass parameters and return results. (In this case, we want to call a function that takes no parameters and returns no results so it will alter only the EAX register and flags.)
- The C environment assumes that the memory selector registers, DS and ES, point to the DGROUP data segment and that the CPU direction flag is clear.
; Assembler wrapper installed as ISR to call C function.
; Return address and flags are already saved on stack on entry
push eax ; Save registers on stack
mov ax,DGROUP ; Prepare to call C…
mov ds,ax ; DS = DGROUP
mov es,ax ; ES = DGROUP
cld ; Clear direction flag
call _ISR1Handler ; Call C function
pop es ; Restore registers
iretd ; Return from interrupt (also restores flags)
—- isr.c —-
/* Body of interrupt handler, called by assembler wrapper */
/* Handle the interrupt here */
There's a little extra overhead in processing an ISR this way, but that overhead can be insignificant if you're writing a complex interrupt handler. Remember to enable compiler optimizations to ensure that the high-level interrupt handler is compiled efficiently.