Skip to content

Interrupts

Three things can interrupt the processor, and each of them jumps through a pointer in RAM that you are allowed to change.

VectorAddressFires when
IRQ_PTR$0300A card asks for attention — a key, a serial byte, a timer
BRK_PTR$0302Your program executes BRK
NMI_PTR$0304The 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 break report; 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:

  1. Checks whether this was a BRK rather than a hardware interrupt, and if so hands over to whatever BRK_PTR points at.
  2. 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.
  3. Asks the keyboard card the same about each of its two ports.
  4. 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

Chaining your own interrupt handler in front of the Kernal’s One interrupt, two handlers A card wants attention a key, a serial byte, the timer The processor reads $0300 whatever address is in there Your handler count it, flash something, whatever it is The Kernal's handler drains the ports into the ring buffer rti back to whatever was running yours, in front Push nothing here. The handler below reads the stack at a fixed depth. Save the old address, install yours, jump to the old one when you are done.
Your handler runs first and then hands on. Replace the Kernal's instead of chaining to it and the keyboard stops working.
asm
; 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-VDP.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", $00
RUN
TYPE SOMETHING AND PRESS ENTER
HELLO
THE PROCESSOR WAS INTERRUPTED 6 TIMES

OK
Type something and press Enter. The count is how many times your handler ran. Open the full emulator

Five letters and an Enter: six characters, six interrupts.

Down a serial line the count comes out lower

Typing at the machine's own keyboard, every character is an interrupt. Over a serial console the Kernal echoes each character back up the line, and while it is doing that it looks at the port itself and keeps anything that has landed — so a character that arrives during the echo of the one before it costs no interrupt at all. Nothing is lost; it simply did not need waking up for. Run this over a cable and the number is three or four, and not the same number twice.

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 one is specific to this machine, and it is the one that bites:

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 stops your program with a break report it never asked for.

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.

A handler starts in binary, with interrupts off

The processor sets I and clears D as it takes the vector, and it does that after pushing the flags — so decimal mode cannot leak into your handler, nothing else can interrupt it until you say so, and rti hands both flags back exactly as the interrupted code left them. The cld that 6502 handlers open with is not needed here.

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.

The video card can interrupt too — at the end of every picture, at a chosen line of the screen, or when sprites overflow or collide — though it never does until a program asks it to.

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?".

asm
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.

The video card's interrupts

The card can interrupt at four moments: when it finishes drawing a picture — sixty times a second — when it reaches a chosen line of the screen, when too many sprites share a line, and when two sprites touch. It does none of them until a program sets the matching bit of its IRQEN register.

Talking to the card from a handler has a trap the other chips don't. Every command to the card is two writes, and the Kernal and BASIC are sending those all the time, with interrupts on. A handler that used the same addresses could arrive between the two halves of one, and both would be garbled. So the card has a second set of addresses, port B at $9C02$9C03, that the Kernal never touches. A handler that uses port B can't collide with anything, and there is no need to turn interrupts off around video work.

This program counts the pictures the card draws in one second:

asm
; Counting pictures: the video card's interrupt, handled on port B.
;
; The card can interrupt at the end of every picture it draws. This program
; asks it to, counts the interrupts for one second, and says how many arrived.
; The handler reads the card through port B, which the Kernal never uses, so it
; can never land between the two halves of a command the Kernal is sending on
; port A.

.setcpu "65C02"

.include "6502-VDP.inc"

.segment "CODE"

BasicStartup:
  .byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00

Start:
  jsr KernalVersion             ; A = major version
  cmp #2
  bcc NoCard
  jsr VdpInfo                   ; carry set: no 6502-PICOVDP
  bcs NoCard

  ; Print first. The screen comes up on the first thing printed, and setting it
  ; up switches the card's interrupts off, so they are switched on after.
  lda #<Counting
  ldy #>Counting
  jsr PrintStr

  sei                           ; two bytes of IRQ_PTR change together
  lda IRQ_PTR
  sta OldIrq
  lda IRQ_PTR+1
  sta OldIrq+1
  lda #<FrameIrq
  sta IRQ_PTR
  lda #>FrameIrq
  sta IRQ_PTR+1

  lda #VC_STAT1                 ; port B's status address reads STAT1 ...
  sta VC_REG2
  lda #VC_REG_WRITE | VC_REG_STATSEL_B
  sta VC_REG2                   ; ... set through port B's own command address

  stz Frames
  lda #VC_IRQ_VBLANK            ; interrupt at the end of every picture
  ldx #VC_REG_IRQEN
  jsr VdpWriteReg
  cli

  lda #100                      ; one second: 100 hundredths
  ldx #0
  jsr SysDelay

  lda #0                        ; the card stops interrupting ...
  ldx #VC_REG_IRQEN
  jsr VdpWriteReg
  sei                           ; ... and the Kernal gets its vector back
  lda OldIrq
  sta IRQ_PTR
  lda OldIrq+1
  sta IRQ_PTR+1
  cli

  lda Frames
  ldx #0
  jsr PrintDecU16
  lda #<Pictures
  ldy #>Pictures
  jmp PrintStr

NoCard:
  lda #<NeedsCard
  ldy #>NeedsCard
  jmp PrintStr

; Every interrupt on the machine comes here first. Only the card's is counted,
; and everything goes on to the Kernal's handler with the stack as it arrived.
FrameIrq:
  pha
  lda VC_STATUS2                ; STAT1: which of the card's interrupts fired,
  lsr a                         ;   and reading it acknowledges them
  bcc @chain                    ; bit 0, the end of a picture, into carry
  inc Frames
@chain:
  pla
  jmp (OldIrq)

Counting:  .byte "COUNTING PICTURES FOR ONE SECOND", CHAR_CR, CHAR_LF, $00
Pictures:  .byte " PICTURES", CHAR_CR, CHAR_LF, $00
NeedsCard: .byte "NEEDS BIOS 2 AND A 6502-PICOVDP", CHAR_CR, CHAR_LF, $00

OldIrq:    .word $0000
Frames:    .byte $00
RUN
COUNTING PICTURES FOR ONE SECOND
60 PICTURES

OK

Or 61, now and then: the second starts part way through a picture.

A second of the video card's interrupts, counted by a handler that never touches the Kernal's side of the card. Open the full emulator

Four details make it work.

The handler reads STAT1. Port B's status address is set to show status register 1, which says which of the card's interrupts are waiting — bit 0 for the end of a picture — and reading it is also how the handler tells the card it has been dealt with. It clears nothing the foreground might be waiting for.

It pushes A and pulls it again before chaining. That is the rule from the warning above, kept: the stack is as the processor left it when the jmp happens.

The interrupt is switched on after the first thing is printed. Bringing the text screen up — which happens on the first output — sets IRQEN to zero, and so does anything else that calls InitVideo. A program that uses the card's interrupts switches them on after the screen is up, and again after anything that puts text mode back.

VBANK and VINC belong to both ports. This handler never changes them. A handler that does must put them back before it returns, because the Kernal's next command on port A depends on them.

BRK

BRK is a software interrupt, and on this machine it stops the program and reports it:

BREAK $07 AT $0A04
A=2A X=03 Y=00 P=30 S=FB

The address is the BRK itself, $07 is the byte after it — yours to use as a breakpoint number — and the second line is every register as the BRK found it, with S the stack pointer from before the processor pushed anything. Then BASIC's prompt comes back, with the program in memory still there. That makes BRK a breakpoint you can leave in a program and a debugging tool that needs no debugger. Reaching the machine has one to run.

The registers stay behind after the report, for a program or for PEEK:

NameAddressHolds
BRK_A, BRK_X, BRK_Y$0313$0315A, X and Y
BRK_P$0310The flags
BRK_SP$0316The stack pointer
BRK_PCL, BRK_PCH$0311$0312The address the processor pushed: the BRK plus two

If a program had taken the video card out of text mode, the report puts the console back first, so it is always readable.

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. A cartridge has to: the report ends by going back to BASIC, and a cartridge has no BASIC to go back to.

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:

asm
  cli
Wait:
  wai                           ; sleeps here until something happens
  lda Flag
  beq Wait

It needs .setcpu "W65C02" and an assembler new enough to accept that — Installing cc65 has the details.

Next: what's fitted.

Written for BIOS v2.0. Released under the MIT License.