CPU interrupts

From NESdev Wiki
Jump to navigationJump to search

An interrupt is a signal that causes a CPU to pause, save what it was doing, and call a routine that handles the signal. The signal usually indicates an event originating in other circuits in a system. The routine, called an interrupt service routine (ISR) or interrupt handler, processes the event and then returns to the main program.

The NES CPU is based on the MOS 6502 CPU, which supports two kinds of interrupts: the ordinary interrupt request (IRQ) and a non-maskable interrupt (NMI). This page covers detailed interrupt behavior for the 6502 and assumes basic familiarity with 6502 interrupts. For a basic introduction to 6502 interrupts, see e.g. the MOS 6502 Programming Manual.

Detailed interrupt behavior

The NMI input is edge-sensitive (reacts to high-to-low transitions in the signal) while the IRQ input is level-sensitive (reacts to a low signal level). Both inputs are active low.

The NMI input is connected to an edge detector. This edge detector polls the status of the NMI line during φ2 of each CPU cycle (i.e., during the second half of each cycle) and raises an internal signal if the input goes from being high during one cycle to being low during the next. The internal signal goes high during φ1 of the cycle that follows the one where the edge is detected, and stays high until the NMI has been handled.

The IRQ input is connected to a level detector. If a low level is detected on the IRQ input during φ2 of a cycle, an internal signal is raised during φ1 the following cycle, remaining high for that cycle only (or put another way, remaining high as long as the IRQ input is low during the preceding cycle's φ2).

The output from the edge detector and level detector are polled at certain points to detect pending interrupts. For most instructions, this polling happens during the final cycle of the instruction, before the opcode fetch for the next instruction. If the polling operation detects that an interrupt has been asserted, the next "instruction" executed is the interrupt sequence.

Many references will claim that interrupts are polled during the last cycle of an instruction, but this is true only when talking about the output from the edge and level detectors. As can be deduced from above, it's really the status of the interrupt lines at the end of the second-to-last cycle that matters.

If both an NMI and an IRQ are pending at the end of an instruction, the NMI will be handled and the pending status of the IRQ forgotten (though it's likely to be detected again during later polling).

The interrupt sequences themselves do not perform interrupt polling, meaning at least one instruction from the interrupt handler will execute before another interrupt is serviced.

Delayed IRQ response after CLI, SEI, and PLP

The RTI instruction affects IRQ inhibition immediately. If an IRQ is pending and an RTI is executed that clears the I flag, the CPU will invoke the IRQ handler immediately after RTI finishes executing. This is due to RTI restoring the I flag from the stack before polling for interrupts.

The CLI, SEI, and PLP instructions on the other hand change the I flag after polling for interrupts (like all two-cycle instructions they poll the interrupt lines at the end of the first cycle), meaning they can effectively delay an interrupt until after the next instruction. For example, if an interrupt is pending and the I flag is currently set, executing CLI will execute the next instruction before the CPU invokes the IRQ handler.

Verification and testing of this behavior in emulators can be accomplished through test ROM images.

Branch instructions and interrupts

The branch instructions have more subtle interrupt polling behavior. Interrupts are always polled before the second CPU cycle (the operand fetch), but not before the third CPU cycle on a taken branch. Additionally, for taken branches that cross a page boundary, interrupts are polled before the PCH fixup cycle (see [1] for a tick-by-tick breakdown of the branch instructions). An interrupt being detected at either of these polling points (including only being detected at the first one) will trigger a CPU interrupt.

Interrupt hijacking

Normally, the NMI vector is at $FFFA, the reset vector at $FFFC, and the IRQ and BRK vector at $FFFE. This means that when servicing an IRQ, the 6502 will push PC and P and then read the new program counter from $FFFE and $FFFF, as if JMP ($FFFE). But the MOS 6502 and by extension the 2A03/2A07 has a quirk that can cause an interrupt to use the wrong vector if two different interrupts occur very close to one another.

For example, if NMI is asserted during the first four ticks of a BRK instruction, the BRK instruction will execute normally at first (PC increments will occur and the status word will be pushed with the B flag set), but execution will branch to the NMI vector instead of the IRQ/BRK vector:

Each [] is a CPU tick. [...] is whatever tick precedes the BRK opcode fetch.

Asserting NMI during the interval marked with * causes a branch to the NMI routine instead of the IRQ/BRK routine:

     ********************
[...][BRK][BRK][BRK][BRK][BRK][BRK][BRK]

In a tick-by-tick breakdown of BRK, this looks like

 #  address R/W description
--- ------- --- -----------------------------------------------
 1    PC     R  fetch opcode, increment PC
 2    PC     R  read next instruction byte (and throw it away),
                increment PC
 3  $0100,S  W  push PCH on stack, decrement S
 4  $0100,S  W  push PCL on stack, decrement S
*** At this point, the signal status determines which interrupt vector is used ***
 5  $0100,S  W  push P on stack (with B flag set), decrement S
 6   $FFFE   R  fetch PCL, set I flag
 7   $FFFF   R  fetch PCH

Similarly, an NMI can hijack an IRQ, and an IRQ can hijack a BRK (though it won't be as visible since they use the same interrupt vector). The tick-by-tick breakdown of all types of interrupts is essentially like that of BRK, save for whether the B bit is pushed as set and whether PC increments occur.

This is not usually a problem for an IRQ interrupted by an NMI because the IRQ will normally still be asserted when the NMI returns and generate a new interrupt. The BRK instruction, however, can effectively be cancelled by an NMI (or an IRQ) this way, so code utilizing BRK should be careful not to have a chance of coinciding with an NMI. (The B bit on the stack can be checked in the NMI handler to detect and dispatch the missing BRK.)

IRQ and NMI tick-by-tick execution

For exposition and to emphasize similarity with BRK, here's the tick-by-tick breakdown of IRQ and NMI (derived from Visual 6502). A reset also goes through the same sequence, but suppresses writes, decrementing the stack pointer thrice without modifying memory. This is why the I flag is always set on reset.

 #  address R/W description
--- ------- --- -----------------------------------------------
 1    PC     R  fetch opcode (and discard it - $00 (BRK) is forced into the opcode register instead)
 2    PC     R  read next instruction byte (actually the same as above, since PC increment is suppressed. Also discarded.)
 3  $0100,S  W  push PCH on stack, decrement S
 4  $0100,S  W  push PCL on stack, decrement S
*** At this point, the signal status determines which interrupt vector is used ***
 5  $0100,S  W  push P on stack (with B flag *clear*), decrement S
 6   A       R  fetch PCL (A = FFFE for IRQ, A = FFFA for NMI), set I flag
 7   A       R  fetch PCH (A = FFFF for IRQ, A = FFFB for NMI)

Notes

  • The above interrupt hijacking and IRQ response behavior is tested by the cpu_interrupts_v2 test ROM.
  • For more details of how the PPU generates NMI during VBlank, see NMI and PPU frame timing.
  • The B status flag doesn't physically exist inside the CPU, and only appears as different values being pushed for bit 4 of the saved status bits by PHP, BRK, and NMI/IRQ.
  • For a more technical description of what causes the hijacking behavior, see Visual6502's writeup.