The 8086 Interrupt Mechanism: The 8259A PIC


Introduction

The original 8088/8086 PCs used an Intel 8259A PIC (Programmable Interrupt Controller) to manage its eight hardware interrupts (also called IRQs, which is short for Interrupt Requests). This separate chip communicates with the processor and tells it when an interrupt needs to be serviced and which ISR (Interrupt Service Routine) to call. Therefore, any device that generates interrupts is connected to one the interrupt pins on the PIC, not to the CPU. The interrupt (or IRQ) pins on the PIC are numbered 0 to 7 where IRQ 0 is the highest priority interrupt and IRQ 7 is the lowest priority. The PIC connects to the processor's single maskable interrupt pin. If an interrupt occurs, the PIC lets the processor know by asserting this interrupt pin.


The Interrupt Flag

One of the flags in the 8086 processor is the interrupt flag, usually referred to as IF. This flag can be set or cleared with the sti and cli instructions respectively. When set (1), the processor monitors its interrupt pin. When it is cleared (0), the processor ignores the signals on its interrupt pin. Therefore, interrupts can be enabled or disabled by setting and resetting the IF flag. However, this essentially disables communication between the PIC and the processor, disabling all external interrupts. To disable individual interrupts, a mask value can be written to the PIC instead. However, this will not be necessary for this class.


The 8259A's Registers

The 8259A has three 8-bit registers that determine its behavior: the IMR (Interrupt Mask Register), the ISR (In-Service Register), and the IRR (Interrupt Request Register). The bits of these registers are numbered 0 through 7, where 0 is the least significant and 7 is the most significant bit. Each bit of each of these registers corresponds to the respective interrupt pin on the PIC. That is, bit 7 corresponds to IRQ 7, bit 6 corresponds to IRQ 6, and so on.

The IMR. This register allows the programmer to disable or "mask" individual interrupts so that the PIC doesn't interrupt the processor when the corresponding interrupt is signaled. For an interrupt to be disabled, its corresponding bit in the IMR must be 1. To be enabled, its bit must be 0. Interrupts can be enabled or disabled by the programmer by reading the IMR, setting or clearing the appropriate bits, then writing the new value back to the IMR.

The IRR. This register indicates when an interrupt has been signaled by a device. As soon as a device signals an interrupt, the corresponding bit in the IRR is set to a 1. This register can only be modified by the PIC and its contents are usually not important to the programmer. However, it can be used to tell which interrupts are waiting for service.

The ISR. This register indicates which interrupts are currently being serviced (i.e., which ISRs have begun execution and have not yet finished). A 1 bit indicates that the corresponding ISR is currently in-service. Several interrupts can be in-service at the same time because of interrupt nesting. The PIC uses this register to determine the highest priority of the interrupts currently being serviced. With this information, the PIC will only interrupt the processor if the highest priority set bit in the IRR has a higher priority than the highest priority set bit in the ISR. In other words, the PIC will never interrupt an in-service interrupt in order to service another interrupt of the same or lower priority. Before an ISR finishes executing, it must send to the PIC the end of interrupt command (EOI) so that the PIC knows that it can safely clear the highest priority bit in the ISR and signal any other pending interrupts. Be careful not to confuse "In-Service Register" with "Interrupt Service Routine", both of which use the "ISR" acronym.


Processor/PIC Actions

When an interrupt occurs, if the interrupt is enabled in the IMR (i.e., it's IMR bit is 0) and if the processor's IF flag is set (IF=1), then hardware does the following:

  1. The PIC informs the processor that an interrupt has occurred by asserting the processor's interrupt pin.
  2. The processor finishes the currently executing instruction.
  3. The processor sends an acknowledgement signal to the PIC.
  4. The PIC then passes to the processor the vector number for the interrupt that occurred.
  5. The processor uses this vector number to determine the address where the ISR (interrupt service routine) is stored. The vector number is used as an index into the interrupt vector table (or interrupt descriptor table), which starts at address 0:0. The corresponding entry in the interrupt vector table contains the address (segment and offset) for the ISR.
  6. The processor pushes the flags, CS, and IP onto the stack (in that order).
  7. The processor clears IF, disabling interrupts.
  8. The processor then sets CS and IP to the segment and offset of the ISR that was read from the vector table and begins execution.

The vector number that is passed to the CPU by the PIC corresponds to the interrupt that occurs. IRQ 0 corresponds to vector number 8, IRQ 1 corresponds to vector number 9, and so on.


ISR Actions

Once execution begins in the ISR, your code that makes up the ISR should do the following (in the order given):

  1. Save the context of whatever was running by pushing all registers onto the stack, except SP, SS, CS, IP, and the flags.
  2. Enable interrupts to allow higher priority IRQs to interrupt.
  3. Run the interrupt handler, which is usually a C function that does the work for this interrupt.
  4. Disable interrupts.
  5. Send the EOI command to the PIC, informing it that the handler is finished.
  6. Restore the context of what was running before the interrupt occurred by popping the registers saved in step 1 off the stack.
  7. Execute the iret instruction. This restores in one atomic operation the values for IP, CS, and the flags, which were automatically saved on the stack by the processor when the interrupt occurred. This effectively restores execution to the point where the interrupt occurred.

Important note: When using an RTOS (Real-Time Operating System) it is also usually necessary to call an OS function after step 1 to inform the OS that an ISR has begun execution. Then another OS function must be called after step 5 to inform the RTOS that the ISR is finished. In the RTOS you will build, these functions correspond to YKEnterISR() and YKExitISR(). This will allow the RTOS to know when execution is supposed to return to task code and allows the RTOS to return control to the highest prority ready task rather than the interrupted task.

Remember that the handling of the IRR and ISR registers in the PIC is done for you automatically as interrupts occur. Additionally, you do not need to read or set the IMR since the PIC will automatically ignore interrupts of the same or lower priority while an interrupt is being serviced. For the labs in this class, if you do not wish to use an interrupt, you should simply create an ISR that does not call an interrupt handler.

Do not push or pop the registers SP or SS when saving and restoring context! This is not necessary since SS and SP must already be correct before executing "pop ss" or "pop sp". Also, the "push sp" instruction behaves differently on some 8086 clones. In an RTOS it will be necessary to save and restore SS and SP as part of a task's context, but this should be handled in a different way (e.g., the mov instruction).

The EOI Command:

In order for interrupts to work correctly, the PIC must be told when the end of an interrupt has occurred. To tell the PIC that the end of an interrupt has occurred we send it what is called a nonspecific EOI command (or unspecific EOI). To do this we simply write the value 0x20 to port 0x20, which is one of the PIC's ports.

	mov	al, 0x20	; Load nonspecific EOI value (0x20) into register al
	out	0x20, al	; Write EOI to PIC (port 0x20)
This code should be executed by every ISR right before it restores the context and executes the iret instruction.