From: "Martin Peach"
Guide: Intercepting And Processing Hardware
Interrupts
www.delorie.com/djgpp/doc/ug/interrupts/hwirqs.html
search
Written by Peter Marinov.
Edited by Martin Peach.
Beginner's guide. This describes what hardware interrupts are and how are they processed by an IBM-compatible PC. If you have already worked with handling hardware interrupts you can skip this part.
Contemporary computers maintain many and varied devices -- hard disk, video card, keyboard, mouse, printer etc. Most of the time these devices work autonomously and require no special CPU attention.
For example, the keyboard needs to inform the CPU only when a key is pressed or released; most of the time the CPU is unaware of what is happening with this device. When a key is pressed the keyboard REQUESTS special attention from the CPU; when the current instruction is completed the processor temporarily discontinues executing the main program and starts ("vectors to") a special chunk of code which will process the keyboard's request. This piece of code obtains the code representing the key that was pressed and informs the keyboard device that the information has been processed, which enables the keyboard to perform a new request when a new key is available. When the request has been resolved the main program continues executing from precisely the point where it was interrupted.
Such a request has a 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 a driver. Such a driver is in most cases provided by the operating system, the device manufacturer, or, rarely, by the BIOS.
As more than one device can request an interrupt at any time, there is a special controller provided in every IBM PC compatible, the Intel 8259 Interrupt Controller (actually there are two of them, and in most modern PCs they have been incorporated into a so-called "chipset" which incorporates several I/O-related subsystems into one physical package). The 8259 serves two purposes in general: to accumulate requests and to convey them one by one to the CPU. Reading the complete documentation of this controller will easily distract you from the IRQ processing material, but following the 8259 processes using examples will make it easier to understand the nature of IRQs. Let us have two requests at one and the same time: the first will be the PC timer and the second will be the keyboard. Each device makes a request to the 8259 for CPU time. The 8259 accepts the requests in a fixed order of priority. First in line is the timer whose interrupt has higher priority than that of the keyboard. When the timer driver serves the request it issues an end-of-interrupt command to the 8259, which directs the controller to signal the next lower proprity interrupt, in this case the keyboard, to the CPU. The keyboard interrupt in turn sends the same end-of-interrupt command, which then makes the 8259 available to accumulate further interrupt requests. Below is how an example of how timer and keyboard drivers might look in a piece of code.
void TimerDriver(void) { ++TimerCount; outportb(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 */ outportb(0x20, inportb(0x20)); /* End-Of-Interrupt command */ }
It is not necessary to actually know how the End-Of-Interrupt command works, you only need to know that such a command is necessary for the successful accomplishment of a hardware interrupt.
The CPU can be directed not to serve hardware interrupts by calling the function disable(). A call to enable() will re-enable the interrupts at the CPU level. The 8259 can be directed not to convey specific device interrupt requests to the CPU. The example below demonstrates the enabling and disabling of timer interrupts.
void DisableTimer(void) { outportb(0x21, inportb(0x21) | 1);/* mask off the timer interrupt */ } void EnableTimer(void) { outportb(0x21, inportb(0x21) & ~1); }
For each device connected to the 8259 controller there is a bit which when set to 1 disables and when set to 0 enables this particular device. From a software point of view this is all that needs to be known for the 8259 interrupt controller. Below is a table showing interrupt priorities for a typical PC. The devices 0-7 are connected to the primary 8259 and 8-15 are connected to the secondary interrupt controller. The 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 sources may vary depending on the installed devices and their configuration.
Advanced guide to handling hardware interrupts under a 32-bit DPMI server.
When compiled using DJGPP your program runs in 32-bit protected-mode, but since DOS is a real-mode operating system, a special kind of executive is needed to provide an environment for 32-bit programs -- the DPMI server (DPMI stands for DOS Protected Mode Interface). While providing smooth execution of 32-bit code, the DPMI host switches to real-mode to execute DOS API code, and switches back to protected-mode to continue executing your code.
IRQs are handled differently in real and protected mode. The 80x86 family of CPUs maintain an interrupt vector table starting at 0x0:0x0. The DOS API provides two functions to attach and detach interrupt handlers for a specific vector in this table. As part of the process of indicating to the CPU that an IRQ needs service the 8259 controller issues an interrupt vector. Using this information the CPU fetches the address of the appropriate driver routine. The execution of the current program is temporarily discontinued ("interrupted"). The CPU stores its current address and flag status onto the stack and then starts to execute the driver code. When the driver has finished handling the interrupt the CPU continues execution of the current program by retrieving the current address and flags from the stack. The 8259 vectors are mapped in the CPU interrupt vector table starting by default at #8.
When running in 32-bit mode the CPU maintains an Interrupt Descriptor Table (IDT) which differs from the interrupt vector table used in real mode. The DPMI host directs the CPU to use the IDT instead of the interrupt vector table. The list of drivers is kept in a 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 a protected mode driver, the CPU starts to execute 32-bit code. The DPMI host guarantees that, even when an IRQ occurs while the CPU is executing real-mode code, the correct 32-bit driver will be executed. If your program runs in any kind of v86 mode environment (EMM386 or QEMM in config.sys, or under a Windows DOS prompt) an IRQ will be served much faster if the driver is 32-bit code.
The DPMI host provides virtual memory, meaning that portions of code or data memory can be "paged out" to a swap file on a disk when not in use, and brought back when this particular code or data is requested by the program in the course of its execution. As DOS is not reentrant in its API functions the disk operations are not reentrant either. Which, in other words, means you should never call disk read/write operations while the previous one has not terminated. This prevents 32-bit driver code being randomly read in or paged out to a disk swap file. The DPMI host provides a set of functions to ensure that a specified portion of code or data should be excluded from the paging process, by designating those memory regions locked. All function and data portions accessed by the driver code should be locked before claiming an IRQ, otherwise the code might not be there when the interrupt routine is called.
When invoking your driver code to handle an IRQ the DPMI host provides only a bare minimum of stack space. It is your responsibility to switch to larger space for stack use. Your driver should save upon entering and restore upon exiting all CPU registers and flags. All segment registers should then be loaded with their proper selectors in order to access driver data (and stack).
As there is a list of tasks that every interrupt handling routine should perform on receiving a request and again on finishing the request, a special wrapper module will be presented below. Understanding this module needs special knowledge in 80386 assembler and AT&T style assembler. However, if you are not in possesion of such advanced skills, you may consider this module a black box, as its functionality will be described in the best possible detail.
The wrapper's functionality is described in exactly 9 steps.
All the steps revealed in details:
Below is the actual code of a 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 secured 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 /* set CPU interrupt enable flag */ ; \ 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 macro and then multiplicated to generate code for all the 16 IRQ wrappers. All the wrappers' addresses fill an array (_IRQWrappers) so that a particular wrapper could be simply picked out by its index in this array.
To prevent the code and data accessed by an IRQ handler from being paged out, the memory should be locked. The DPMI server provides a __dpmi_lock_linear_region() function for locking memory regions. Below is the code for the 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(®ion) == -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(®ion) == -1) return (-1); return (0); }
_my_cs() and _my_ds() return the code and data segment selectors respectively. The 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 the code size is unknown? When compiled, two adjacent functions occupy two adjacent memory regions. Now calculating function code size becomes simple address 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 macros 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, this time using macros.
long TimerCount; void TimerDriver(void) { ++TimerCount; outportb(0x20, inportb(0x20)); /* End-Of-Interrupt command */ } END_OF_FUNCTION(TimerDriver); ... LOCK_FUNCTION(TimerDriver); LOCK_VARIABLE(TimerCount);
Intercepting IRQs involves a pair of DPMI functions -- __dpmi_get_protected_mode_interrupt_vector() and __dpmi_set_protected_mode_interrupt_vector(). The original vector value is stored in OldIRQVectors[], to be used by the wrapper to invoke the old handler, and to be restored when UninstallIRQ() is called. The 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. The setup function InitIRQ() is called once only, to prepare the library for proper use. UninstallIRQ() keeps count of how much IRQ vectors are hooked, and when the last one is released, a ShutDownIRQ() cleanup function is invoked to release stack space from the heap. Notice that interrupt vectors for the second (cascaded) IRQ controller are mapped starting at position 0x70. Depending on the IRQ number the correct vector will 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(); }
InitIRQ() and ShutDownIRQ() are a pair of functions, called when InstallIRQ() is first called and when UninstallIRQ unhooks the last vector that was intercepted. The InitIRQ() function locks all the data and code memory accessed from an IRQ call, allocates space for stacks, and sets a flag indicating that the module is properly set up. The ShutDownIRQ() function disposes of the stack space.
static int InitIRQ(void) { int i; /* Lock IRQWrapers[], IRQHandlers[] and IRWrap0()-IRQWrap15(). */ if (LOCK_VARIABLE(IRQWrappers) == -1) return 0; if (LOCK_VARIABLE(IRQHandlers) == -1) return 0; if (LOCK_VARIABLE(OldIRQVectors) == -1) return 0; if (LOCK_FUNCTION(IRQWrap) == -1) return 0; for (i = 0; i < IRQ_STACKS; ++i) { if ((IRQStacks[i] = malloc(STACK_SIZE)) == NULL) ... LockData(IRQStacks[i], STACK_SIZE) == -1 ... (char *)IRQStacks[i] += (STACK_SIZE - 16); /* Stack is incremented downward */ } bInitIRQ = 1; return 1; } static void ShutDownIRQ(void) { int i; char *p; for (i = 0; i < IRQ_STACKS; ++i) { p = (char *)IRQStacks[i] - (STACK_SIZE - 16); free(p); } bInitIRQ = 0; }
Allegro, A game programming
library originated by Shawn Hargreaves
Alaric B. Williams, The Dark Art of
writing DJGPP Hardware Interrupt Handlers
webmaster donations bookstore | delorie software privacy |
Copyright © 1998 by DJ Delorie | Updated Nov 1998 |
You can help support this site by visiting the advertisers that sponsor it! (only once each, though)