NMI: Difference between revisions
(→Race condition: People might miss that line at the very end, so move it up.) |
(→Race condition: tokumaru appears adamant about mentioning the one weakness of this method) |
||
Line 49: | Line 49: | ||
rti | rti | ||
</pre> | </pre> | ||
But even this handler is not perfect. | |||
If your game code takes significantly longer than 24,000 cycles, such as if you have too many critters moving on the screen, it may take longer than one frame. | |||
Waiting for NMI in this way would miss an NMI that happens while other code is running. | |||
In some cases, this could cause a sprite 0-triggered scroll split to flicker (or worse). | |||
Gradius counts the approximate time that each object handler takes and deliberately overflowing the calculations to the next frame when it might otherwise come close to missing an NMI or a sprite 0 hit. | |||
Games developed by [[wikipedia:Micronics|Micronics]] are likely to reduce the overall frame rate below 60 frames per second to match the worst case of lag. |
Revision as of 19:23, 28 March 2010
The 2A03 and most other 6502 family CPUs are capable of processing a non-maskable interrupt (NMI). This input is edge-sensitive, meaning that if other circuitry on the board pulls the /NMI pin from high to low voltage, this sets a flip-flop in the CPU. When the CPU checks for interrupts and find that the flip-flop is set, it pushes the processor status register and return address on the stack, reads the NMI handler's address from $FFFA-$FFFB, clears the flip-flop, and jumps to this address.
"Non-maskable" means that no state inside the CPU can prevent the NMI from being processed as an interrupt. However, most boards that use a 6502 CPU's /NMI line allow the CPU to disable the generation of /NMI signals by writing to a memory-mapped I/O device. In the case of the NES, the /NMI line is connected to the NES PPU and used to detect vertical blanking.
Operation
Two 1-bit registers inside the PPU control the generation of NMI signals. Frame timing and accesses to the PPU's PPUCTRL ($2000) and PPUSTATUS ($2002) registers change these registers as follows:
- Start of vertical blanking: Set NMI_occurred in PPU to true.
- End of vertical blanking, sometime in pre-render scanline: Set NMI_occurred to false.
- Read $2002: Return old status of NMI_occurred in bit 7, then set NMI_occurred to false.
- Write to $2000: Set NMI_output to bit 7.
The PPU pulls /NMI low if and only if both NMI_occurred and NMI_output are true. By toggling NMI_output ($2000 bit 7) during vertical blank without reading $2002, a program can cause /NMI to be pulled low multiple times, causing multiple NMIs to be generated.
Caveats
Old emulators
Some platforms, such as the Game Boy, keep a flag turned on through the whole vertical blanking interval. Some early emulators such as NESticle were developed under the assumption that $2002.7 worked the same way and thus do not turn off NMI_occurred in line 3. Thus, some defective homebrew programs developed in this era will wait for $2002.7 to become false and expect this to happen at the end of vblank. (The right way to wait for the end of vblank involves triggering a sprite 0 hit and waiting for that flag to become 0.) Some newer homebrew programs have been known to display a diagnostic message if an emulator incorrectly returns true on two consecutive reads of $2002.7 .
Race condition
If 1 and 3 happen simultaneously, $2002.7 is read as false, and NMI_occurred is set to false anyway. This means that the following code that waits for vertical blank by spinning on $2002.7 is likely to miss an occasional frame:
wait_status7: bit $2002 bpl wait_status7 rts
Code like wait_status7
is fine while your program is waiting for the PPU to warm up.
But once the game is running, the most reliable way to wait for a vertical blank is to turn on NMI_output and then wait for the NMI handler to set a variable:
wait_nmi: lda retraces @notYet: cmp retraces beq @notYet rts nmi_handler: inc retraces rti
But even this handler is not perfect. If your game code takes significantly longer than 24,000 cycles, such as if you have too many critters moving on the screen, it may take longer than one frame. Waiting for NMI in this way would miss an NMI that happens while other code is running. In some cases, this could cause a sprite 0-triggered scroll split to flicker (or worse).
Gradius counts the approximate time that each object handler takes and deliberately overflowing the calculations to the next frame when it might otherwise come close to missing an NMI or a sprite 0 hit. Games developed by Micronics are likely to reduce the overall frame rate below 60 frames per second to match the worst case of lag.