Interrupts
Three things can interrupt the processor, and each of them jumps through a pointer in RAM that you are allowed to change.
| Vector | Address | Fires when |
|---|---|---|
IRQ_PTR | $0300 | A card asks for attention — a key, a serial byte, a timer |
BRK_PTR | $0302 | Your program executes BRK |
NMI_PTR | $0304 | The non-maskable interrupt line is pulled |
At power-on the Kernal points all three at its own handlers. IRQ_PTR goes to the routine that empties the serial port and the keyboards into the ring buffer; BRK_PTR goes to the Monitor; NMI_PTR goes to an rti.
What the machine's own handler does
Every time a key is pressed, a serial byte arrives, or the timer that SysDelay uses runs out, the processor stops what it is doing and runs the Kernal's interrupt handler. It:
- Checks whether this was a
BRKrather than a hardware interrupt, and if so hands over to the Monitor. - Asks the serial port whether it has a byte; if it has, puts it in the ring buffer, and raises RTS if the buffer is filling up.
- Asks the keyboard card the same about each of its two ports.
- Returns.
None of that is optional — take it out and typing stops working. So the way to add your own is to put yourself in front of it.
Chaining
; Counting interrupts — a handler of your own, hooked in front of the one the
; machine already runs.
;
; Every key that arrives interrupts the processor. This program adds a link to
; the front of that chain, counts what goes past, then unhooks itself and says
; how many it saw. The counting handler is one instruction long on purpose:
; the handler behind it reads the processor status off the stack at a fixed
; depth, so a link in the chain must leave the stack exactly as it found it.
.setcpu "65C02"
.include "6502.inc"
.segment "CODE"
BasicStartup:
.byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00
Start:
stz Count
lda #<Prompt
ldy #>Prompt
jsr PrintStr
sei ; never swap a vector with interrupts live
lda IRQ_PTR
sta Chain
lda IRQ_PTR + 1
sta Chain + 1
lda #<CountOne
sta IRQ_PTR
lda #>CountOne
sta IRQ_PTR + 1
cli
Waiting:
jsr Chrin ; carry set = a key was waiting, already echoed
bcc Waiting
cmp #CHAR_CR
bne Waiting
sei ; put the old handler back
lda Chain
sta IRQ_PTR
lda Chain + 1
sta IRQ_PTR + 1
cli
jsr PrintCRLF
lda #<Saw
ldy #>Saw
jsr PrintStr
lda Count
ldx #0
jsr PrintDecU16
lda #<Times
ldy #>Times
jsr PrintStr
jsr PrintCRLF
rts
; The new front of the chain. INC touches no register and pushes nothing, so
; the handler it hands over to finds the stack the way the processor left it.
CountOne:
inc Count
jmp (Chain)
Count: .byte 0
Chain: .word 0
Prompt: .byte "TYPE SOMETHING AND PRESS ENTER", CHAR_CR, CHAR_LF, $00
Saw: .byte "THE PROCESSOR WAS INTERRUPTED ", $00
Times: .byte " TIMES", $00RUN
TYPE SOMETHING AND PRESS ENTER
HELLO
THE PROCESSOR WAS INTERRUPTED 6 TIMES
OKFive letters and an Enter: six characters, six interrupts.
Three things in there are the whole technique.
Save the old vector, install yours, put it back when you are done. A program that returns to BASIC leaving IRQ_PTR pointing into its own code will work perfectly until the next thing loads over it.
sei while you swap. Two bytes have to change, and an interrupt arriving between them jumps through half of each address.
Push nothing. This is the one that is specific to this machine, and it is worth reading twice.
Your link must leave the stack exactly as it found it
The Kernal's handler works out whether it was called by BRK by reading the saved status register off the stack at a fixed depth — past the three registers it has just pushed itself. If your handler pushes anything before jumping to it, that arithmetic lands on the wrong byte and every hardware interrupt looks like a BRK, which sends the machine into the Monitor.
So a chained handler either uses only instructions that touch no register — inc, dec, stz on absolute addresses are the useful ones — or it saves and restores everything it used before the jmp. It does not leave anything on the stack.
If you want to do real work in an interrupt, the way around that is to replace rather than chain: take the vector entirely, do your work, push and pull as much as you like, and end with rti. You then own the serial port and the keyboards too — everything the Kernal's handler was doing. That is a reasonable thing for a game to do; it is not a reasonable thing to do by accident.
Where interrupts come from
The keyboard card's VIA is the busiest source: one interrupt per key, on either port. The serial chip raises one per received byte. The same VIA's timer 1 is what SysDelay counts on, and the clock card can be set to interrupt at a chosen time.
All of them arrive on the same line and land in the same handler, which is why a handler's first job is always to ask each chip "was it you?".
MyHandler:
lda GPIO_IFR ; the card's interrupt flag register
and #GPIO_INT_CB1
beq NotMine
; ... it was, deal with it — reading the port clears the flag
NotMine:
jmp (Chain)A flag you do not clear fires for ever
Every source has to be told it has been dealt with, and how depends on the chip: reading the port clears the keyboard flags, reading the data register clears the serial one, writing the timer's latch clears the timer's. Miss one and the processor spends the rest of its life in your handler.
BRK
BRK is a software interrupt, and on this machine it lands in the Monitor with every register on display. That makes it a breakpoint you can leave in a program and a debugging tool that needs no debugger — X at the Monitor's dot prompt gets you back to BASIC.
Point BRK_PTR at your own routine and you have caught it instead. The processor has already pushed the status register and the return address, and your handler needs to know that the address is the BRK plus two.
WAI, if your assembler will let you
The W65C02S has an instruction that stops the processor until an interrupt arrives. As a way to wait it is both instant to wake from and dramatically cheaper than a polling loop:
cli
Wait:
wai ; sleeps here until something happens
lda Flag
beq WaitIt needs .setcpu "W65C02" and an assembler new enough to accept that — Installing cc65 has the details.
Next: what's fitted.

