The C64 has three interrupt types:
| Type | Vector | Description | Maskable? |
|---|---|---|---|
| IRQ (Interrupt ReQuest) | $FFFE/$FFFF → $0314/$0315 | Hardware timer, VIC raster | Yes (SEI/CLI) |
| NMI (Non-Maskable Interrupt) | $FFFA/$FFFB → $0318/$0319 | RESTORE key, expansion port | No |
| BRK (Break) | Same as IRQ | Software interrupt instruction | Yes |
The default IRQ at $EA31 performs (in order):
The IRQ is triggered approximately 60 times per second (NTSC) or 50 times per second (PAL) by CIA #1 Timer A.
Additionally, the VIC-II chip can generate IRQs on:
CIA #1 ($DC00-$DCFF) Timer A:
VIC-II ($D000-$D3FF):
SEI ; disable interrupts while modifying vector
LDA #<MYIRQ ; install our handler
STA $0314 ; CINV low
LDA #>MYIRQ
STA $0315 ; CINV high
CLI ; re-enable interrupts
RTS
MYIRQ ; Your code here (save/restore registers if needed)
; A, X, Y are already saved by Kernal if coming through $0314
INC $D020 ; flash border (example)
JMP $EA31 ; chain to default IRQ handler (jiffy, keyboard, etc.)
SEI
LDA #<MYIRQ
STA $0314
LDA #>MYIRQ
STA $0315
CLI
RTS
MYIRQ PHA ; save A, X, Y ourselves
TXA
PHA
TYA
PHA
; ... your time-critical code ...
PLA
TAY
PLA
TAX
PLA
RTI ; note: RTI, NOT RTS
CIA #1 is at $DC00-$DCFF.
| Address | Name | Description |
|---|---|---|
| $DC04 | TIMALO | Timer A latch/counter low byte |
| $DC05 | TIMAHI | Timer A latch/counter high byte |
| $DC0D | CIAICR | Interrupt control register |
| $DC0E | CIACRA | Control register A |
; Set CIA #1 Timer A for 1/100 second interval (NTSC: ~10226 cycles)
LDA #$4E ; 0x4E = 78 low byte of 10000 = $2710
STA $DC04 ; Timer A low latch
LDA #$27 ; high byte
STA $DC05 ; Timer A high latch
LDA #$81 ; enable Timer A interrupt
STA $DC0D ; interrupt control (bit 7=1=enable, bit 0=timer A)
LDA #$11 ; start timer, continuous, system clock source
STA $DC0E ; control register A
; Disable default CIA #1 Timer A IRQ (keyboard/cursor will stop working!)
LDA #$7F ; bit 7=0 means disable
STA $DC0D ; disable timer A interrupt
Reading $DC0D returns the interrupt status and clears it:
Bit 7: (read) 1=any interrupt occurred
Bit 4: FLAG interrupt
Bit 3: Serial shift register
Bit 2: Timer B
Bit 1: Timer A
Bit 0: Time of Day alarm
Writing $DC0D sets/clears interrupt enables:
Bit 7: 1=enable the bits that follow; 0=disable
Bits 0-4: individual interrupt sources
The VIC-II generates an IRQ when the electron beam reaches the scan line stored in:
NTSC: lines 0-261 (263 total), PAL: lines 0-311 (312 total) Visible area begins approximately line 50 (NTSC) or line 48 (PAL).
SEI
; Point IRQ vector to our handler
LDA #<RASTER_IRQ
STA $0314
LDA #>RASTER_IRQ
STA $0315
; Set raster line to trigger on
LDA $D011
AND #$7F ; clear bit 8 of raster
STA $D011
LDA #100 ; trigger at line 100
STA $D012
; Enable VIC raster interrupt, disable CIA IRQ
LDA #$7F
STA $DC0D ; disable CIA #1 timer interrupt
LDA #$01
STA $D01A ; VIC IRQ enable: raster
CLI
RASTER_IRQ:
LDA $D019 ; read VIC interrupt register
STA $D019 ; acknowledge (writing 1 to bit clears it)
; Check it's really a raster IRQ
AND #$01
BEQ NOT_RASTER
; --- Your raster effect code ---
LDA #$02
STA $D020 ; change border color at this scanline
; --------------------------------
NOT_RASTER:
; Manually do jiffy work if CIA IRQ disabled
; (or use a secondary CIA timer IRQ for jiffy)
RTI
The basic raster IRQ can be off by ±1 scanline due to CPU interrupt latency. For demo effects requiring pixel-perfect stability:
Double-IRQ method (from demo scene lore):
; First IRQ fires, then sets a second IRQ for exact same line
; The second one fires at a consistent point in the raster
STAGE1 LDA #<STAGE2
STA $0314
LDA #>STAGE2
STA $0315
LDA #101 ; trigger second IRQ at next line
STA $D012
LDA #$01
STA $D019 ; ack
RTI
STAGE2 ; We are now at a stable point in line 101
LDA #<STAGE1
STA $0314
LDA #>STAGE1
STA $0315
LDA #100
STA $D012
LDA #$01
STA $D019
; ... stable raster effect ...
RTI
The NMI is triggered by the RESTORE key or the NMI line on the expansion port.
Default NMI vector chain:
; Install custom NMI handler (catches RESTORE key)
LDA #<MYNMI
STA $0318 ; NMINV low
LDA #>MYNMI
STA $0319 ; NMINV high
RTS
MYNMI PHA
TXA
PHA
TYA
PHA
; RESTORE key was pressed — handle it
PLA
TAY
PLA
TAX
PLA
RTI
CIA #2 ($DD00) can generate NMIs. Useful for background tasks that should not be stoppable.
| Address | Description |
|---|---|
| $DD04 | Timer A low latch |
| $DD05 | Timer A high latch |
| $DD0D | CIA #2 ICR (read=status, write=enable/disable) |
| $DD0E | CIA #2 Control Register A |
A common pattern for SID music playback:
; 1. Initialize music player (call once)
JSR MUSIC_INIT
; 2. Install IRQ that calls the play routine
SEI
LDA #<MUSIC_IRQ
STA $0314
LDA #>MUSIC_IRQ
STA $0315
CLI
RTS
MUSIC_IRQ:
JSR MUSIC_PLAY ; call one frame of music player
JMP $EA31 ; chain to default (keeps keyboard working)
; MUSIC_INIT and MUSIC_PLAY are from your music player (e.g., GoatTracker)
CIA #1 also has a real-time clock (BCD format):
| Address | Description |
|---|---|
| $DC08 | TOD Tenths of seconds (BCD) |
| $DC09 | TOD Seconds (BCD) |
| $DC0A | TOD Minutes (BCD) |
| $DC0B | TOD Hours + AM/PM flag (bit 7) |
; Read current time
LDA $DC0B ; hours (latch all registers by reading hours first)
LDA $DC0A ; minutes
LDA $DC09 ; seconds
LDA $DC08 ; tenths
; Set time (write hours last to start clock)
LDA #$00 ; tenths
STA $DC08
LDA #$30 ; 30 seconds (BCD)
STA $DC09
LDA #$45 ; 45 minutes (BCD)
STA $DC0A
LDA #$12 ; 12 hours (BCD)
STA $DC0B ; writing hours starts the clock
A technique from The Advanced ML Book: use the IRQ to send printer data in the background.
; Simplified spooler skeleton
; Store data in SPOOLBUF, SPOOLPTR points to current byte, SPOOLEND to end
SPOOLIRQ:
LDA SPOOLPTR
CMP SPOOLEND
BEQ DONE_SPOOLING ; nothing to send
; Send one byte to printer
LDA $DFLTO ; check if printer already busy
CMP #4 ; device 4?
BEQ SKIP_SEND
LDA (SPOOLPTR),Y ; get next byte
JSR $FFD2 ; BSOUT
INC SPOOLPTR
BNE SKIP_SEND
INC SPOOLPTR+1
SKIP_SEND:
DONE_SPOOLING:
JMP $EA31 ; chain to default IRQ
Powered by TurnKey Linux.