Skip to content

(ASM Programming) Interrupts and interrupt handling

Adam Kubiczek edited this page Apr 7, 2023 · 9 revisions

Interrupts and interrupt handling

Address Description
$FFFE (ROM) Universal interrupt vector
$F583 (ROM) Kernal interrupt selector
$0316 (RAM) BRK vector
$0314 (RAM) IRQ vector

What is an interrupt?

An interrupt is a signal to the CPU to stop whatever it's doing and execute the "interrupt handler" at some address in memory. This is usually caused by a device connected to the CPU, such as the video card signalling the end of one displayed frame and the start of another, but can also represent something more direct, like a "reset" button that the user has pressed on the computer's case. They can also be triggered by software, through the brk instruction.

When something triggers an interrupt on the CPU, the CPU is allowed to finish its current instruction before beginning the interrupt handler. Once done, it pushes the program counter to the stack followed by the processor's current status bits. If the interrupt was triggered by a "break" instruction (BRK), or else was caused by a non-maskable interrupt signal, it leaves these status bits alone. For all other interrupts, the processor clears the "Break" bit from the set of status bits that are pushed onto the stack.

The processor then sets the program counter to the 16-bit address stored in $FFFE. As of r33, this address is $F583, which is in ROM.

Interrupts on the X16

Starting from $F583, the Kernal pushes the contents of the A, X, and Y registers, in that order. It then reads the status bits that were pushed to the stack, and checks for the "Break" flag. If that flag exists, it jumps to the 16-bit address stored at $0316 (the BRK handler), otherwise it jumps to the 16-bit address stored at $0314 (the IRQ handler).

Programs can set their own IRQ handlers by overwriting $0314 with the low byte of their handler's address, and $0315 with the high byte of their handler's address. When you do this, make sure interrupts have been suppressed with the sei instruction, first, or your program may crash!

set_custom_irq_handler:
    sei
    lda #<custom_irq_handler
    sta $0314
    lda #>custom_irq_handler
    sta $0315
    cli
    rts

When done, programs have two choices: They can perform a jmp instruction to whatever address the Kernal had previously written to $0314, or it can exit the interrupt process manually.

If the program wishes to jump back to the Kernal's interrupt handling, it must store the values of $0314 and $0315 before overwriting them:

Default_irq_handler: !le16 $0000

preserve_default_irq:
    lda $0314
    sta Default_irq_handler
    lda $0315
    sta Default_irq_handler+1
    rts

At this point, the program can end its IRQ handler by jumping to the Kernal's default handler:

custom_irq_handler:
    ; Whatever code your program
    ; wanted to execute...

    ; Return to Kernal handling:
    jmp (Default_irq_handler)

The Kernal's default handler will ensure that it properly restores all processor values before returning control to the interrupted program code.

If, on the other hand, the program would rather end the IRQ immediately, and return to whatever code was interrupted, it must first restore the registers that Kernal code had pushed onto the stack, followed by an rti instruction:

custom_irq_handler:
    ; Whatever code your program
    ; wanted to execute...

    ; Return to whatever had been interrupted:
    ply
    plx
    pla
    rti

The most common interrupt: VSYNC

By far, the most common interrupt you can expect to handle right now is the VSYNC interrupt generated by the VERA chip. This potentially adds two wrinkles to a custom IRQ handler: First, verifying whether the interrupt was generated by the VERA. Second, if the custom IRQ handler does not jump to the Kernal's default IRQ handler at the end, it must clear the VERA's interrupt signal on behalf of the Kernal.

Detecting the VERA's interrupt requires checking $9F27 for the flag $01, whereas clearing the VERA's interrupt requires writing $01 to $9F27.

Consider the following example where the custom IRQ handles VSYNC:

custom_irq_handler:
    lda $9F27
    and #$01
    beq irq_done

    ; Whatever code your program
    ; wanted to execute...

    lda #$01
    sta $9F27

    ; Return to whatever had been interrupted:
irq_done:
    ply
    plx
    pla
    rti

Line interrupts

The VERA can also generate interrupts at a specified line from 0 to 479. To enable this, first adjust the IRQ_LINE at IRQLINE_L and IEN ($9F28 and $9F26) to the line number you want the interrupt to occur at, then enable the line interrupt on the VERA's $9F26 by OR'ing the flag $02 into it.

Consider this example, in which we set the VERA to generate a line interrupt at line 240 ($F0):

set_interrupt_to_line_240:
    lda #$F0
    sta $9F28
    lda $9F26
    and #%01111111 ; clear the bit 8 of the IRQ_LINE
    sta $9F26

enable_line_interrupt:
   lda $9F26
   ora #$02
   sta $9F26

We can detect this interrupt separately from the VSYNC interrupt by checking $9F27 for the flag $02, and making sure to clear the flag if we chose to handle it:

custom_irq_handler:
    lda $9F27
    and #$02
    beq vsync_interrupt

    ; Whatever code you need to do
    ; on the line interrupt...

    lda #$02
    sta $9F27
    jmp irq_done

vsync_interrupt:
    lda $9F27
    and #$01
    beq irq_done

    ; Whatever code your program
    ; wanted to execute...

    lda #$01
    sta $9F27

    ; Return to whatever had been interrupted:
irq_done:
    ply
    plx
    pla
    rti