A television draws its picture one line at a time, top to bottom, fifty or sixty times a second. A home computer’s video chip reads the same registers for every line, so a colour or scroll value set once applies to the whole screen. Change a register while the picture is being drawn, though, and everything below that point uses the new value. A raster interrupt makes the change happen at the right moment: the video chip interrupts the processor when the beam reaches a chosen line, and a short handler rewrites whatever it needs before the beam moves on.
Commodore said what this was for in the Commodore 64 Programmer’s Reference Guide in 1982: “This powerful interrupt structure lets you use split screen modes. For instance you can have half of the screen bit mapped, half text, more than 8 sprites at a time, etc. The secret is to use interrupts properly.” Five years later ZZAP!64‘s machine-code column, written by Gary Liddon, put it in players’ terms: “all those games that have the top half of the screen scrolling and the bottom half of the screen stationary are using raster interrupts.” The playfield scrolls, and a raster interrupt partway down puts the scroll register back to zero for the score panel.
On the Commodore 64
The VIC-II counts the lines it draws, and compares the count with a value the program has written. When they are equal it sets a flag, and if that source is enabled it pulls the processor’s IRQ line. Four registers do the work:
| Register | What it does here |
|---|---|
$D012 (53266) |
Write: bits 0–7 of the line to interrupt on. Read: bits 0–7 of the current line. |
$D011 (53265), bit 7 |
Bit 8 of the same line number, for lines 256 and above. |
$D019 (53273), bit 0 |
Set when the line matches. Writing a 1 to it clears it. |
$D01A (53274), bit 0 |
Set it to let the raster match cause an interrupt. |
A PAL machine draws 312 lines and an NTSC one 262 or 263, depending on the chip revision, so eight bits cannot name them all; hence the ninth bit in $D011. Lines above 262 exist only on PAL, so an interrupt set for line 280 never happens on an NTSC machine.
The flag in $D019 stays set until the program clears it. The Programmer’s Reference Guide explains that each flag “must be cleared by writing a 1 to that bit in the interrupt register when you’re ready to handle it”. A handler that forgets returns to find the IRQ line still held low, and is called again at once, for ever. Liddon’s readers found the rule odd too: the flag “has to be cleared by bunging an $FF in there, yes I know bunging a zero in would be much more logical, but that’s the way it is.”
The comparison is for equality. If a program sets $D012 to a line the beam has already passed, nothing happens until that line comes round again in the next frame.
Sharing the IRQ line with the KERNAL
The VIC-II is not the only chip that can pull the IRQ line. CIA 1 does it too, about sixty times a second, to drive the KERNAL’s own interrupt routine, which keeps the clock, flashes the cursor and reads the keyboard; see CIA 6526. Mapping the Commodore 64 warns: “There is only one IRQ vector, but there are many sources for IRQ interrupts”. A program that adds a raster interrupt either works out which chip called, or switches the CIA’s interrupt off and calls the KERNAL routine itself once a frame.
The KERNAL gives two ways in. With the ROM switched in, the processor’s hardware vector at $FFFE points to a KERNAL routine at $FF48. It saves A, X and Y on the stack, checks that the interrupt was not a BRK instruction, and jumps through the RAM vector at $0314–$0315, which normally points to $EA31. A program changes $0314–$0315 to point to its own handler. That route costs 29 cycles after the processor’s own 7-cycle interrupt sequence, before the handler’s first instruction runs.
The handler then leaves in one of two ways:
JMP $EA31runs the whole KERNAL routine: clock, cursor, keyboard scan. Then it restores the registers and returns.JMP $EA81skips all that. The five instructions there,PLA,TAY,PLA,TAX,PLA, followed byRTI, restore the registers and return, in 22 cycles.
All three C64 KERNAL revisions have both routines at the same addresses. They are two different exits, not PAL and NTSC versions of one, and the chain below runs unchanged on both kinds of machine.
A chain of interrupts
A split needs at least two interrupts a frame: one to change a register partway down, and one to change it back. Each handler sets up the next, by writing the next line number and pointing the vector at the next handler. The last one points back to the first.
This program colours three bands of the border, with interrupts on lines 100, 180 and 260. The third line is above 255, so the handler before it sets bit 7 of $D011, and the next one clears it again. It switches off CIA 1’s interrupts and lets the last handler of the frame call the KERNAL routine, so the keyboard and cursor keep working at one scan per frame. It returns to BASIC afterwards.
; raster chain through the KERNAL vector at $0314 (ACME syntax)
; start with SYS 2304
* = $0900
start sei ; no interrupts while we change things
lda #$7f
sta $dc0d ; switch off CIA 1's timer interrupts
lda $dc0d ; and clear any that are waiting
lda #<irq1
sta $0314 ; point the KERNAL's IRQ vector at irq1
lda #>irq1
sta $0315
lda #100
sta $d012 ; first interrupt on line 100...
lda $d011
and #$7f
sta $d011 ; ...with bit 8 of the line number clear
lda #$01
sta $d01a ; enable the raster interrupt
sta $d019 ; clear any raster interrupt already flagged
cli
rts
irq1 lda #$01
sta $d019 ; acknowledge
lda #2
sta $d020 ; border red from line 100
lda #180
sta $d012 ; next interrupt on line 180
lda #<irq2
sta $0314
lda #>irq2
sta $0315
jmp $ea81 ; restore registers and return
irq2 lda #$01
sta $d019
lda #7
sta $d020 ; border yellow from line 180
lda #<260
sta $d012 ; next interrupt on line 260 ($104):
lda $d011 ; low byte in $d012,
ora #$80 ; bit 8 in bit 7 of $d011
sta $d011
lda #<irq3
sta $0314
lda #>irq3
sta $0315
jmp $ea81
irq3 lda #$01
sta $d019
lda #14
sta $d020 ; border back to light blue from line 260
lda #100
sta $d012
lda $d011
and #$7f
sta $d011
lda #<irq1
sta $0314
lda #>irq1
sta $0315
jmp $ea31 ; once a frame: the KERNAL's clock, cursor and keyboardLine 260 exists on both PAL and NTSC machines, so the chain runs on either. The border is light blue at the top, red from line 100, yellow from line 180, and light blue again in the lower border. Because the KERNAL routine now runs once per frame, not from its own timer, BASIC’s TI clock counts frames: fifty a second on PAL, sixty on NTSC.
Without the KERNAL
Games that needed the memory, or the cycles, switched the KERNAL ROM out altogether. Liddon’s 1987 example did this “by altering location 1”, the processor port that selects which ROMs are visible. With the ROM gone, the processor fetches its vector from RAM at $FFFE–$FFFF, so the handler starts 7 cycles after the interrupt is taken instead of 36. It has to do the rest itself. The processor saves only the program counter and status register, so the handler saves any register it uses, and returns with RTI.
; raster interrupt through the hardware vector, KERNAL switched out
; (ACME syntax; start it from a BASIC SYS line)
start sei
lda #$35
sta $01 ; RAM at $a000 and $e000; I/O stays in at $d000
lda #$7f
sta $dc0d ; no CIA 1 interrupts (IRQ)
sta $dd0d ; no CIA 2 interrupts (NMI)
lda $dc0d ; clear any that are waiting
lda $dd0d
lda #<irq1
sta $fffe ; the processor's own IRQ vector, now in RAM
lda #>irq1
sta $ffff
lda #<nmi
sta $fffa ; RESTORE would jump through here
lda #>nmi
sta $fffb
lda #100
sta $d012
lda $d011
and #$7f
sta $d011
lda #$01
sta $d01a
sta $d019
cli
main jmp main ; the main program would run here
irq1 pha ; this handler uses only A, so saves only A
lda #$01
sta $d019 ; acknowledge
lda #2
sta $d020 ; border red from line 100
lda #200
sta $d012 ; next interrupt on line 200
lda #<irq2
sta $fffe
lda #>irq2
sta $ffff
pla
nmi rti
irq2 pha
lda #$01
sta $d019
lda #14
sta $d020 ; border light blue from line 200
lda #100
sta $d012
lda #<irq1
sta $fffe
lda #>irq1
sta $ffff
pla
rtiThe RESTORE key is wired to the processor’s NMI line, which SEI cannot block. With the KERNAL gone, its vector at $FFFA would point into whatever RAM lies underneath, so the program points it at an RTI.
What the handler cannot control
The interrupt is on time; the handler is not quite. The processor finishes the instruction it is running before it takes the interrupt, and 6510 instructions take 2 to 7 cycles, so the handler starts a few cycles later in some frames than in others. Each cycle is 8 pixels of screen, so a colour change made this way has a ragged edge. On a badline the VIC-II also stops the processor for 40 to 43 cycles; see the VIC-II entry. For most game splits, made in the border or between rows of graphics, none of this shows. For effects that must land on one exact cycle, programmers built stable raster routines.
The processor sets its interrupt-disable flag when it takes an interrupt, so a second raster interrupt waits until the first handler’s RTI restores the flag. A handler that runs longer than the gap to the next line in the chain misses it, and the next interrupt comes a frame late: the split jumps or flickers. Sprite multiplexers, which may need an interrupt every few lines, check for this before they return; see sprite multiplexing.
On the Amiga
The Amiga’s custom chips have no register that interrupts the processor at a chosen line. They have something better suited to most of the jobs: the copper, a coprocessor that waits for beam positions and writes to the custom chip registers itself, so colour splits and screen splits need no processor time at all.
When the processor does need to act at a particular line, the copper can interrupt it. Commodore’s Hardware Reference Manual says: “To interrupt the 680x0, use the Copper MOVE instruction to store a 1” into bit 4 of INTREQ, the bit called COPER, which “is used by the Copper to issue a level 3 interrupt”. Two instructions in a copper list do it:
dc.w $8007,$fffe ; WAIT for line 128 ($80)
dc.w $009c,$8010 ; MOVE to INTREQ: set bit 4, COPERThe level 3 handler reads INTREQR to see which source called, since vertical blank and the blitter share the level, and clears COPER by writing $0010 to INTREQ. The interrupt reaches the processor only if COPER and the master enable bit are set in INTENA.
On the NES
The NES’s picture chip, the PPU, has no line counter a program can set. It can raise a non-maskable interrupt when vertical blank begins, which is where most games do their screen updates. For a split partway down, a program can wait for the sprite 0 hit flag, as Super Mario Bros. does to hold its status bar still, but that means the processor waits in a loop.
A true mid-screen interrupt came from the cartridge. The MMC3 mapper counts scanlines by watching one of the PPU’s address lines and raises an IRQ when its counter reaches zero. NESdev’s documentation of the chip lists four registers: $C000 holds the number of lines to count, a write to $C001 reloads the counter, a write to $E001 enables the interrupt, and a write to $E000 “will disable MMC3 interrupts AND acknowledge any pending interrupts”. A handler that wants another interrupt later in the frame therefore writes $E000 to acknowledge, then sets the counter and writes $E001 again.
Sources
- Commodore, Commodore 64 Programmer’s Reference Guide (1982): “More sprites can be displayed using RASTER INTERRUPT techniques” (p. 131); the raster register, the interrupt status and enable registers, clearing a flag by writing a 1, split screens and “more than 8 sprites at a time” (pp. 150–152); Appendix N, the interrupt register (p. 447).
- Gary Liddon, machine-code column, “Raster interrupts drive Mr Liddon off the rails”, ZZAP!64 issue 29, September 1987, pp. 68–69: split-screen scrolling, banking out the KERNAL through location 1, enabling with
$D01A, clearing$D019with$FF. - Sheldon Leemon, Mapping the Commodore 64 (COMPUTE! Publications, 1984): the IRQ vector at 788 (
$314), the ROM’s register saving, “only one IRQ vector, but there are many sources” (pp. 72–73); the IRQ entry at 59953 ($EA31) (pp. 219–220). - Commodore 64 KERNAL ROM, revisions 901227-01, -02 and -03, read from the ROM images: the IRQ entry at
$FF48(register saves,BRKtest,JMP ($0314)) and the exit at$EA81(PLA,TAY,PLA,TAX,PLA,RTI), identical in all three. Cycle counts are from the standard 6502 instruction timings. - Christian Bauer, The MOS 6567/6569 video controller (VIC-II) and its application in the Commodore 64 (1996), section 3.4: lines per frame for the 6569, 6567R8 and 6567R56A; section 3.5: badlines stop the processor for 40–43 cycles.
- Synertek, SY6500/MCS6500 Microcomputer Family Programming Manual (August 1976), p. 25: the interrupt disable flag “is set by the microprocessor during reset and interrupt commands”.
- Amiga Hardware Reference Manual, 3rd edition (Commodore-Amiga, Addison-Wesley, 1991): interrupting the 680x0 from the copper (p. 35);
COPERand the level 3 interrupt (p. 219);INTENA,INTREQandINTREQR, the level 3 sources and theINTENmaster bit (Appendix A). - NESdev Wiki, “MMC3” (community reference, web, secondary): the IRQ latch, reload, disable and enable registers. https://www.nesdev.org/wiki/MMC3
The two C64 listings were assembled with ACME and run in the VICE emulator (x64sc) as PAL and NTSC machines.