www.delorie.com/djgpp/doc/ug/interrupts/irqs.html   search  
Guide: Intercepting And Processing Hardware Interrupts

PART I

Begginers guide. This is to describe what are hardware interrupts and how are they processed by an IBM PC compatible. If you have already worked with handling hardware interrupts skip this part.

1. First acquaintance with hardware interrupts.

Contemporary computer is maintaining lots and various of devices -- hard disk, video card, keyboard, mouse, printer etc. Most of the time these devices work autonomously and require none of special CPU attention.

For example keyboard need to inform the CPU only when a key is pressed or released, most of the time CPU knows nothing what happens with this device. When a key is pressed the keyboard REQUESTS special attention by the CPU, when the current instruction is completed it temporarily discontinues executing the main program and starts a special chunk of code to proceed keyboard request. This piece of code gets the key pressed and informs the keyboard device that the information is processed which enables the keyboard to perform new request when new key is available. When the request is resolved the main program continues to be executed exactly at the point where was left.

Such a request has its special name Interrupt Request or IRQ. The CPU maintains a special table with addresses for 15 interrupt requests. The chunk of code resolving an interrupt request is usually called driver. Such a driver for most of the cases is provided by the operating system or device manufacturers or rarely by BIOS.

2. Hardware interrupt controller - 8259.

As more than one device can request interrupt at a time there's a special controller provided in every IBM PC compatible -- Intel 8259 Interrupt Controller (actually there are two of them). 8259 serves two purposes in general to accumulate requests and to convey them one by one to the CPU. Acquiring complete documentation of this controller will easily distract you from IRQ processing material. But following the 8259 processes with a simple example makes it simple to understand the very nature of IRQs. Let's have two requests at a time first to be PC timer and second will be the keyboard. Both devices make a request to 8259 that CPU should serve particular events. 8259 accepts the requests. First is the timer that interrupts the CPU as this device has higher priority than the keyboard. When the timer driver serves the request it issues an end-of-interrupt command to 8259 which directs the controller to initiate keyboard interrupt toward the CPU. The keyboard interrupt driver complies to same end-of-interrupt command which then makes 8259 available to further accumulate interrupt requests. Bellow is how will look timer and keyboard drivers in peace of code.

void TimerDriver(void)
{
  ++TimerCount;
  outpotb(0x20, inportb(0x20));   /* End-Of-Interrupt command */
}

void KbdDriver(void)
{
  unsigned char control_kbd;

  LastKey = inportb(0x60);

  /* Tell the keyboard that the key is processed */
  control_kbd = inportb(0x61);
  outportb(0x61, control_kbd | 0x80);  /* set the "enable kbd" bit */
  outportb(0x61, control_kbd);  /* write back the original control value */
  outpottb(0x20, inportb(0x20));  /* End-Of-Interrupt command */
}

It is not necessary to actually know how End-Of-Interrupt command works, you need to know that such a command is necessary for successful accomplishment of a hardware interrupt.

CPU can be directed not to serve hardware interrupts by disable() and enabled back by enable(). 8259 can be directed not to convey specific device interrupt requests to the CPU. Below is an example that enables and disables timer interrupts to be processed.

void DisableTimer(void)
{
  outportb(0x21, inportb(0x21) | 1);
}

void EnableTimer(void)
{
  outportb(0x21, inportb(0x21) & ~1);
}

For each of the devices connected to 8259 controller there's a bit which when set to 1 disables and set to 0 enables this particular device. By software point of view this is all that should be known for 8259 interrupt controller. Below is a table which shows interrupt priorities. The devices 0-7 are connected to the primary and 8-15 connected to secondary interrupt controller. Secondary interrupt controller can be accessed via ports 0xa0 and 0xa1.

 0 System timer
 1 Keyboard
 2 Cascaded second interrupt controller
 3 COM2 - serial interface
 4 COM1 - serial interface
 5 LPT - parallel interface
 6 Floppy disk controller
 7 Available
 8 CMOS real-time clock
 9 Sound card
10 Network adapter
11 Available
12 Available
13 Numeric processor
14 IDE -- Hard disk interface
15 IDE -- Hard disk interface

The map of interrupt controllers may vary depending on the installed devices and their configuration.

PART II

Advanced guide in handling hardware interrupts under 32-bit DPMI server.

3. IRQ handling in real and 32-bit protected mode (under DPMI).

While compiled with DJGPP your program runs in 32-bit protected mode. But as DOS is real-mode operating system there's a special kind of executives to provide an environment for 32-bit programs -- DPMI servers (DPMI stands for DOS Protected Mode Interface). While providing smooth execution of 32-bit code the DPMI host is switching to real mode to execute DOS API code and switching back to continue executing your code in protected-mode.

There's a certain difference in handling IRQs while in real and protected mode. 80x86 family of CPUs maintain an interrupt vectors table starting at 0x0:0x0. DOS API provides two functions to attach and detach interrupt handlers for a specific vector in this table. By indicating to the CPU that an IRQ is to be served the 8259 controller issues an interrupt vector. By using this information CPU fetches the address of a driver routine. The consecutive execution of the current program flow is temporarily discontinued. The CPU stores the current address and flag status onto the stack and then starts to execute the driver code. When driver accomplishes the CPU continues the current program execution by extracting back the current address and flags from the stack. The 8259 vectors are mapped in the CPU interrupt vector table starting by default at #8.

While running in 32-bit mode the CPU maintains an Interrupt Descriptor Table (IDT) which is different than interrupt vectors table in real mode. The DPMI host directs the CPU to start using IDT instead of interrupt vectors table. The list of drivers is kept in virtual interrupt vector table. When an interrupt occurs the host checks this virtual interrupt vector table and if the IRQ is to be served by protected mode driver the CPU starts to execute 32-bit code. DPMI host guarantees that even when an IRQ occurs while CPU executes real mode code the correct 32-bit driver will be executed. If your program run's in a kind of v86 mode environment (EMM386 or QEMM in config.sys, or under Windows DOS prompt) an IRQ will be served far faster if driver is 32-bit code.

DPMI host provides virtual memory as portions of code or data memory can be paged out on disk when not in use and brought back when this particular code or data is requested by the program flow of execution. As DOS is not reentrant in its API functions the disk operations are not reentrant too. Which said in other words means never to relay on calling disk read/write operation while last one is not accomplished. This prevents 32-bit driver code to be read in or paged out to disk swap file randomly in time. The DPMI host provides set of functions to claim that a portion of code or data should be excluded from paging process, the memory regions are locked. All function and data portions used by driver code should be locked before claiming certain IRQ.

Invoking your driver code to handle an IRQ the DPMI host provides only a bare minimum of stack space. It is you responsibility to switch to more vast space for stack use. Your driver should save upon entering and restore upon exiting all the registers and CPU flags. All segment registers should be loaded with their proper selectors in order to access driver data (and stack).

4. Writing a wrapper for a hardware interrupts handler.

As there is list of tasks that each interrupt handling routine should perform receiving request a special wrapper module will be presented below. Understanding this module needs special knoledge in 80386 assembler and AT&T style assembler. However being not in possesion of such advanced skils you may consider this module a black box as its functionality will be described in best possible details.

Wrapper functionality is described in exactly 9 steps.

  1. save all 80386 registers that may be destroyed by the IRQ handler
  2. let all segment registers to point handler's data segment by loading the proper selector
  3. stack pointer register should be in disposition of enough stack area
  4. invoke user IRQ handler
  5. restore original stack settings
  6. restore all 80386 registers
  7. if directed call original IRQ handler (being default upon program start)
  8. disable interrupts as DPMI server is expecting
  9. exit with "return from interrupt" instruction

All the steps revealed in details:
1 - save all 80386 registers. This is done by simply pushing all the resigters in the stack. At step 7 they will be poped back.
2 - load segment registers. In DJGPP all segments by default point to same segment (flat memory). Thus all may be loaded by a single selector value. DJGPP provides a cs relative variable that is all time IRQ sequred selector -- __djgpp_ds_alias.
3 - let stack pointer sp point to an area of enough stack space. Wrapper should be provided a table of readiliy allocated stacks -- _IRQStacks. The wrapper searches for an available stack. If successfull marks the stack as used, this will prevent other IRQs to allocate the same stack. If stack search is not successfull the wrapper exits IRQ unserved. This may lead to computer lock-up, that's why enough stacks should be ensured in _IRQStacks.
4 - calls proper IRQ handler. IRQ handler call-back functions should be set in _IRQHandlers array. As the wrapper nows its IRQ number the proper handler is invoked.
5 - restore the original stack frame, old stack frame was store in the new stack. Pop the old frame and load ss:sp with this value.
6 - restore all 80386 CPU registers. Pop the registers from stack.
7 - the handler is a call-back function in format int IRQHandler(void). If the handler returns 1 the default IRQ handler is to be called. Before attaching particular wrapper to interrupt vector old value should be stored in _OldIRQVectors table. The wrapper jumps to particular old vector instead proceeding with step 9, the original old handler will finish the interrupt request with an "return-from-interrupt" intstruction. Before jumping the CPU registers are restored from the stack.
8 - disable the interrupts before returning to DPMI. DPMI expects the interrupts disabled upon returning.
9 - Finish by an "return-from-interrupt" instruction.

Below is the actual code of wrapper module.

/* wrap_g.S */

/*
IRQ wrappers for DJGPP.
*/

.data
_IRQWrappers:
        .long   _IRQWrap0,  _IRQWrap1,  _IRQWrap2,  _IRQWrap3
        .long   _IRQWrap4,  _IRQWrap5,  _IRQWrap6,  _IRQWrap7
        .long   _IRQWrap8,  _IRQWrap9,  _IRQWrap10, _IRQWrap11
        .long   _IRQWrap12, _IRQWrap13, _IRQWrap14, _IRQWrap15

_IRQHandlers:
        .long   0, 0, 0, 0      /* 0 - 3 */
        .long   0, 0, 0, 0      /* 4 - 7 */
        .long   0, 0, 0, 0      /* 8 - 11 */
        .long   0, 0, 0, 0      /* 12 - 15 */

        .globl  _IRQWrappers
        .globl  _IRQHandlers
        .globl  _IRQWrap
        .globl  _IRQWrap_End

/*
How many stacks to allocate for the irq wrappers. You could 
probably get away with fewer of these, if you want to save memory and
you are feeling brave...
Extracted from irqwrap.h: BOTH SHOULD BE THE SAME!
*/
#define IRQ_STACKS      8

.text
#define IRQWRAP(x)                                                      ; \
_IRQWrap##x:                                                            ; \
        pushw   %ds                     /* save registers */            ; \
        pushw   %es                                                     ; \
        pushw   %fs                                                     ; \
        pushw   %gs                                                     ; \
        pushal                                                          ; \
        /* __djgpp_ds_alias is irq sequred selector (see exceptn.h) */  ; \
        movw    %cs:___djgpp_ds_alias, %ax                              ; \
        movw    %ax, %ds                /* set up selectors */          ; \
        movw    %ax, %es                                                ; \
        movw    %ax, %fs                                                ; \
        movw    %ax, %gs                                                ; \
                                                                        ; \
        movl    $(IRQ_STACKS - 1), %ecx /* look for a free stack */     ; \
        /* Search from the last toward the first */                     ; \
StackSearchLoop##x:                                                     ; \
        leal    _IRQStacks(, %ecx, 4), %ebx                             ; \
        cmpl    $0, (%ebx)                                              ; \
        jnz     FoundStack##x           /* found one! */                ; \
                                                                        ; \
        decl    %ecx                    /* backward */                  ; \
        jnz     StackSearchLoop##x                                      ; \
                                                                        ; \
        jmp     GetOut##x               /* No free stack! */            ; \
                                                                        ; \
FoundStack##x:                                                          ; \
        movl    %esp, %ecx              /* save old stack in ecx:dx */  ; \
        movw    %ss, %dx                                                ; \
                                                                        ; \
        movl    (%ebx), %esp            /* set up our stack */          ; \
        movw    %ax, %ss                                                ; \
                                                                        ; \
        movl    $0, (%ebx)              /* flag the stack is in use */  ; \
                                                                        ; \
        pushl   %edx                    /* push old stack onto new */   ; \
        pushl   %ecx                                                    ; \
        pushl   %ebx                                                    ; \
                                                                        ; \
        cld                             /* clear the direction flag */  ; \
                                                                        ; \
        movl    _IRQHandlers + 4 * x, %eax                              ; \
        call    *%eax                   /* call the C handler */        ; \
                                                                        ; \
        popl    %ebx                    /* restore the old stack */     ; \
        popl    %ecx                                                    ; \
        popl    %edx                                                    ; \
        movl    %esp, (%ebx)                                            ; \
        movw    %dx, %ss                                                ; \
        movl    %ecx, %esp                                              ; \
                                                                        ; \
        orl     %eax, %eax              /* check return value */        ; \
        jz      GetOut##x                                               ; \
                                                                        ; \
        popal                           /* chain to old handler */      ; \
        popw    %gs                                                     ; \
        popw    %fs                                                     ; \
        popw    %es                                                     ; \
        popw    %ds                                                     ; \
                                        /* 8 = sizeof(__dpmi_paddr) */  ; \
        ljmp    %cs:_OldIRQVectors + 8 * x                              ; \
                                                                        ; \
GetOut##x:                                                              ; \
        popal                           /* iret */                      ; \
        popw    %gs                                                     ; \
        popw    %fs                                                     ; \
        popw    %es                                                     ; \
        popw    %ds                                                     ; \
        sti                                                             ; \
        iret

_IRQWrap:
        .byte   0

IRQWRAP(0);
IRQWRAP(1);
IRQWRAP(2);
IRQWRAP(3);
IRQWRAP(4);
IRQWRAP(5);
IRQWRAP(6);
IRQWRAP(7);
IRQWRAP(8);
IRQWRAP(9);
IRQWRAP(10);
IRQWRAP(11);
IRQWRAP(12);
IRQWRAP(13);
IRQWRAP(14);
IRQWRAP(15);

_IRQWrap_End:
        .byte   0

The file is "wrap_g.S". 'S' is capital to direct gcc to provide preprocessor support for the file. The wrapper is defined as a macros and then multiplicated to generate code for all the 16 IRQ wrappers. All the wrappers addresses fill an array (_IRQWrappers) so particular wrapper could be simply picked up by its index in this array.

5. Code and data locking.
To prevent from paging the code and data accessed by an IRQ handler the memory should be locked. DPMI server provides a function __dpmi_lock_linear_region() for locking memory regions. Below is the code of LockData() and LockCode() functions.

int LockData(void *a, long size)
{
  unsigned long baseaddr;
  __dpmi_meminfo region;

  if (__dpmi_get_segment_base_address(_my_ds(), &baseaddr) == -1)
    return (-1);

  region.handle = 0;
  region.size = size;
  region.address = baseaddr + (unsigned long)a;

  if (__dpmi_lock_linear_region(&region) == -1)
    return (-1);

  return (0);
}

int LockCode(void *a, long size)
{
  unsigned long baseaddr;
  __dpmi_meminfo region;

  if (__dpmi_get_segment_base_address(_my_cs(), &baseaddr) == -1)
    return (-1);

  region.handle = 0;
  region.size = size;
  region.address = baseaddr + (unsigned long)a;

  if (__dpmi_lock_linear_region(&region) == -1)
    return (-1);

  return (0);
}

_my_cs() and _my_ds() returns code and data segment selectors respectively. Region description structure is filled with start and end addresses and then __dpmi_lock_linear_region() is invoked.

Locking a variable or array looks like this LockData(&var, sizeof(var)), but how to lock a function when code size is unknown? When compiled two adjacent functions occupy two adjacent memory regions. Now calculating function code size is a simple addresses arithmetic. Below is an example of how to lock an IRQ handler function.

void TimerDriver(void)
{
  ++TimerCount;
  outpotb(0x20, inportb(0x20));  /* End-Of-Interrupt command */
}

void EndOfTimerDriver(void)
{
}

...

LockCode(TimerDriver, (long)EndOfTimerDriver - (long)TimerDriver);

It is useful to use macroses to facilitate data and code locking.

#define END_OF_FUNCTION(x)    static void x##_End() { }

#define LOCK_VARIABLE(x)  LockData((void *)&x, sizeof(x))
#define LOCK_FUNCTION(x)  LockCode(x, (long)x##_End - (long)x)

Below is the same example of function and data locking but using these macroses.

long TimerCount;

void TimerDriver(void)
{
  ++TimerCount;
  outpotb(0x20, inportb(0x20));  /* End-Of-Interrupt command */
}
END_OF_FUNCTION(TimerDriver);

...

LOCK_FUNCTION(TimerDriver);
LOCK_VARIABLE(TimerCount);
6. Intercepting IRQs, allocating stacks (ancillary service functions).

Intercepting IRQs involves a pair of DPMI functions -- __dpmi_get_protected_mode_interrupt_vector() and __dpmi_set_protected_mode_interrupt_vector(). The old vector value is stored in OldIRQVectors[] to be user by the wraper to invoke the old handler and to be restored when UninstallIRQ() is called. Wrapper will invoke the proper user code based on what is loaded in IRQHandlers[]. The proper IRQ wrapper is picked from IRQWrappers[] which is initialized in wrap_g.S. Only once is called a setup function InitIRQ() (source code not listed here) to alloc and lock stacks space in the heap. UninstallIRQ() keeps count of how much IRQ vectors are intercepted and when the last one is released a cleaning function is invoked -- ShutDownIRQ() (source code not listed here) to release stacks from the heap. Notice that interrupt vectors for the second (cascaded) IRQ controller are mapped starting at position 0x70. Depending on IRQ number the proper vector is intercepted.

int InstallIRQ(int nIRQ, int (*IRQHandler)(void))
{
  int nIRQVect;
  __dpmi_paddr IRQWrapAddr;

  if (!bInitIRQ)
    if (!InitIRQ())
      return 0;

  if (nIRQ > 7)
    nIRQVect = 0x70 + (nIRQ - 8);
  else
    nIRQVect = 0x8 + nIRQ;

  IRQWrapAddr.selector = _my_cs();
  IRQWrapAddr.offset32 = (int)IRQWrappers[nIRQ];
  __dpmi_get_protected_mode_interrupt_vector(nIRQVect, &OldIRQVectors[nIRQ]);
  IRQHandlers[nIRQ] = IRQHandler;  /* IRQWrapper will call IRQHandler */
  __dpmi_set_protected_mode_interrupt_vector(nIRQVect, &IRQWrapAddr);
  return 1;
}

void UninstallIRQ(int nIRQ)
{
  int nIRQVect;
  int i;

  if (nIRQ > 7)
    nIRQVect = 0x70 + (nIRQ - 8);
  else
    nIRQVect = 0x8 + nIRQ;

  __dpmi_set_protected_mode_interrupt_vector(nIRQVect, &OldIRQVectors[nIRQ]);
  IRQHandlers[nIRQ] = NULL;

  /*
  Check whether all the IRQs are uninstalled and call ShutDownIRQ().
  */
  for (i = 0; i < 16; ++i)
    if (IRQHandlers[i] != NULL)
      return;  /* Still remains a handler */
  ShutDownIRQ();
}

7. Acknoledgements (you may consider this "for further reading" as well).
Allegro, Game programming library originated by Shawn Hargreaves
--http://www.talula.demon.co.uk/index.html
Alaric B. Williams, The Dark Art of writing DJGPP Hardware Interrupt Handlers
--http://www.abwillms.demon.co.uk/prog/djints.txt

8. Source code.
Complete source code, a makefile and a small test program is available for downloading at xxx.

  webmaster     delorie software   privacy  
  Copyright © 1998     Updated Nov 1998