17. DOSEMU Programmable Interrupt Controller

This emulation, in files picu.c and picu.h, emulates all of the useful features of the two 8259 programmable interrupt controllers. Support includes these i/o commands:

ICW1 bits 0 and 1

number of ICWs to expect

ICW2 bits 3 - 7

base address of IRQs

ICW3 no bits

accepted but ignored

ICW4 no bits

accepted but ignored

OCW1 all bits

sets interrupt mask

OCW2 bits 7,5-0

EOI commands only

OCW3 bits 0,1,5,6

select read register, select special mask mode

Reads of both PICs ports are supported completely.

17.1. Other features

17.2. Caveats

OCW2 support is not exactly correct for IRQs 8 - 15. The correct sequence is that an OCW2 to PIC0 enables IRQs 0, 1, 3, 4, 5, 6, and 7; and an OCW2 to PIC1 enables IRQs 8-15 (IRQ2 is really IRQ9). This emulation simply enables everything after two OCW2s, regardless of which PIC they are sent to.

OCW2s reset the currently executing interrupt, not the highest priority one, although the two should always be the same, unless some dos programmer tries special mask mode. This emulation works correctly in special mask mode: all types of EOIs (specific and non-specific) are treated as specific EOIs to the currently executing interrupt. The interrupt specification in a specific EOI is ignored.

Modes not supported:

None of these modes is useable in a PC, anyway. The 16 additional interrupts are run in Auto-EOI mode, since any dos code for them shouldn't include OCW2s. The level 0 (NMI) interrupt is also handled this way.

17.3. Notes on theory of operation:

The documentation refers to levels. These are priority levels, the lowest (0) having highest priority. Priority zero is reserved for use by NMI. The levels correspond with IRQ numbers as follows:

      IRQ0=1  IRQ1=2   IRQ8=3  IRQ9=4  IRQ10=5 IRQ11=6 IRQ12=7 IRQ13=8
      IRQ14=9 IRQ15=10 IRQ3=11 IRQ4=12 IRQ5=13 IRQ6=14 IRQ7=15

There is no IRQ2; it's really IRQ9.

There are 16 more levels (16 - 31) available for use by dosemu functions.

If all levels were activated, from 31 down to 1 at the right rate, the two functions run_irqs() and do_irq() could recurse up to 15 times. No other functions would recurse; however a top level interrupt handler that served more than one level could get re-entered, but only while the first level started was in a call to do_irq(). If all levels were activated at once, there would be no recursion. There would also be no recursion if the dos irq code didn't enable interrupts early, which is quite common.

17.3.1. Functions supported from DOSEMU side

17.3.1.1. Functions that Interface with DOS:

unsigned char read_picu0(port), unsigned char read_picu1(port)

should be called by the i/o handler whenever a read of the PIC i/o ports is requested. The "port" parameter is either a 0 or a 1; for PIC0 these correspond to i/o addresses 0x20 and 0x21. For PIC1 they correspond with i/o addresses 0xa0 and 0xa1. The returned value is the byte that was read.

void write_picu0(port,unsigned char value), void write_picu1(port,unsigned char value)

should be called by the i/o handler whenever a write to a PIC port is requested. Port mapping is the same as for read_picu, above. The value to be written is passed in parameter "value".

int do_irq()

is the function that actually executes a dos interrupt. do_irq() looks up the correct interrupt, and if it points to the dosemu "bios", does nothing and returns a 1. It is assumed that the calling function will test this value and run the dosemu emulation code directly if needed. If the interrupt is revectored to dos or application code, run_int() is called, followed by a while loop executing the standard run_vm86() and other calls. The in-service register is checked by the while statement, and when the necessary OCW2s have been received, the loop exits and a 0 is returned to the caller. Note that the 16 interrupts that are not part of the standard AT PIC system will not be writing OCW2s; for these interrupts the vm86 loop is executed once, and control is returned before the corresponding dos code has completed. If run_int() is called, the flags, cs, and ip are pushed onto the stack, and cs:ip is pointed to PIC_SEG:PIC_OFF in the bios, where there is a hlt instruction. This is handled by pic_iret.

Since the while loop above checks for interrupts, this function can be re-entered. In the worst case, a total of about 1k of process stack space could be needed.

pic_sti(), pic_cli()

These are really macros that should be called when an sti or cli instruction is encountered. Alternately, these could be called as part of run_irqs() before anything else is done, based on the condition of the vm86 v_iflag. Note that pic_cli() sets the pic_iflag to a non-zero value, and pic_sti() sets pic_iflag to zero.

pic_iret()

should be called whenever it is reasonably certain that an iret has occurred. This call need not be done at exactly the right time, its only function is to activate any pending interrupts. A pending interrupt is one which was self-scheduled. For example, if the IRQ4 code calls pic_request(PIC_IRQ4), the request won't have any effect until after pic_iret() is called. It is held off until this call in an effort to avoid filling up the stack. If pic_iret() is called too soon, it may aggravate stack overflow problems. If it is called too late, a self-scheduled interrupt won't occur quite as soon. To guarantee that pic_iret will be called, do_irq saves the real return address on the stack, then pushes the address of a hlt, which generates a SIGSEGV. Because the SIGSEGV comes before the hlt, pic_iret can be called by the signal handler, as well as from the vm86 loop. In either case, pic_iret looks for this situation, and pops the real flags, cs, and ip.

17.3.2. Other Functions

void run_irqs()

causes the PIC code to run all requested interrupts, in priority order. The execution of this function is described below.

First we save the old interrupt level. Then *or* the interrupt mask and the in-service register, and use the complement of that to mask off any interrupt requests that are inhibited. Then we enter a loop, looking for any bits still set. When one is found, it's interrupt level is compared with the old level, to make sure it is a higher priority. If special mask mode is set, the old level is biased so that this test can't fail. Once we know we want to run this interrupt, the request bit is atomicly cleared and double checked (in case something else came along and cleared it somehow). Finally, if the bit was actually cleared by this code, the appropriate bit is set in the in-service register. The second pic register (for PIC1) is also set if needed. Then the user code registered for this interrupt is called. Finally, when the user code returns, the in-service register(s) are reset, the old interrupt level is restored, and control is returned to the caller. The assembly language version of this funcction takes up to about 16 486 clocks if no request bits are set, plus 100-150 clocks for each bit that is set in the request register.

void picu_seti(unsigned int level,void (*func), unsigned int ivec)

sets the interrupt vector and emulator function to be called then the bit of the interrupt request register corresponding to level is set. The interrupt vector is ignored if level<16, since those vectors are under the control of dos. If the function is specified as NULL, the mask bit for that level is set.

void picu_maski(int level)

sets the emulator mask bit for that level, inhibiting its execution.

void picu_unmask(int ilevel)

checks if an emulator function is assigned to the given level. If so, the mask bit for the level is reset.

void pic_watch()

runs periodically and checks for 'stuck' interrupt requests. These can happen if an irq causes a stack switch and enables interrupts before returning. If an interrupt is stuck this way for 2 successive calls to pic_watch, that interrupt is activated.

17.4. A (very) little technical information for the curious

There are two big differences when using pic. First, interrupts are not queued beyond a depth of 1 for each interrupt. It is up to the interrupt code to detect that further interrupts are required and reschedule itself. Second, interrupt handlers are designated at dosemu initialization time. Triggering them involves merely specifying the appropriate irq.

Since timer interrupts are always spaced apart, the lack of queueing has no effect on them. The keyboard interrupts are re-scheduled if there is anything in the scan_queue.