-
Notifications
You must be signed in to change notification settings - Fork 60
(ASM Programming) Interrupts and interrupt handling
Address | Description |
---|---|
$FFFE | (ROM) Universal interrupt vector |
$F583 | (ROM) Kernal interrupt selector |
$0316 | (RAM) BRK vector |
$0314 | (RAM) IRQ vector |
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.
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
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
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