Entering and Exiting Interrupts

Posted on

By Kodiak


If you're new to 6502 coding on the Commodore 64 or, at least, new to working with interrupts, you've probably seen the "textbook" way to enter and exit them.

And sure, it's not without good reason it's done that way... it reinforces the need for the programmer to be mindful of certain things that must be taken care of as part of any interrupt code, specifically, recording and recovering the registers and acknowledging the interrupts themselves.

But textbook - or "cargo cult" in the lingo of the coding snobs - is not necessarily optimal in many cases.

However, before elaborating on why that's the case, let's consider the typically taught way of entering an interrupt:

Textbook way to enter an interrupt
(3+2+3+2+3 = 13 cycles to record registers, takes 5 bytes of RAM)

PHA ; [3] Push contents of A-register (Accumulator) on to Stack
TXA ; [2] Since there is no PHX instruction, do X -> A
PHA ; [3] Push contents of X-register on to Stack via A-reg
TYA ; [2] Since there is no PHY instruction, do Y -> A
PHA ; [3] Push contents of Y-register on to Stack via A-reg
; Interrupt's proper tasks begin here


Plainly, therefore, the above does 3 things:

  1. It stores the contents of the Accumulator out of harm's way for later retrieval at the end of the interrupt.
  2. It stores the contents of the X-register out of harm's way for later retrieval at the end of the interrupt.
  3. It stores the contents of the Y-register out of harm's way for later retrieval at the end of the interrupt.


Then, at the end of the interrupt, the standard thing to do is recover the registers in the following fashion, which is cognisant of the first-in, last-out way of using the Stack for storing values:

Textbook way to exit an interrupt
(4+2+4+2+4 = 16 cycles to restore registers, takes 5 bytes of RAM)

; Interrupt acknowledged before this point
PLA ; [4] Pull contents of Stack into Y-reg via A-reg
TAY ; [2] Since there is no PLY instruction, do A -> Y
PLA ; [4] Pull contents of Stack into X-reg via A-reg
TAX ; [2] Since there is no PLX instruction, do A -> X
PLA ; [4] Pull contents of Stack into A-reg
RTI ; Return from interrupt


These actions ensure that, when the interrupt finally finishes and the CPU returns to executing the code in the program's "main loop", all the registers are restored to the exact condition they were in just before the interrupt started.

Now, this becomes a waste of code and CPU cyles when the interrupt handler code only uses 1 or 2, but not all 3 registers, so to enter and exit efficiently, we should only record and recover the actual registers the interrupt handler code uses, as per the example below:

Simple interrupt handler using only A-register
(3 cycles to record A-register, 4 cycles to recover, 2 bytes of RAM)

PHA ; [3] Push contents of A-register on to Stack
LDA #%00001110 ; Set charset to $7800
STA $D018
LDA #06 ; Set background colour = BLUE
STA $D021
LDA #<IRST6 ; Set vectors for next handler
STA $FFFE
LDA #>IRST6
STA $FFFF
LDA #$80 ; Set trigger point for next handler
STA $D012
ASL $D019 ; Acknowledge interrupt
PLA ; [4] Pull contents of Stack into A-reg
RTI ; Return from interrupt


Since the X-reg and Y-reg were not used in the main handler code, there was no need to record and restore them, thus saving 10 CPU cycles on entering the interrupt handler and 12 on exiting it (22 cycles altogether which is approximately one third of a raster line saved); the RAM saved is 4 bytes on entry and 4 on exit = 8 overall.

And if you're not doing anything exotic like interrupting the interrupt handler using the NMI, you could avoid the Stack altogether and just replace the opening PHA with STA ZPHOLDA, where ZPHOLDA is a zero page (or "Zeropage") value that will be used to temporarily hold the A-reg's value on entering the interrupt handler; then, at the end of the handler, you would replace PLA with LDA ZPHOLDA.

That process takes 2 more bytes of RAM overall than just using the PHA/PLA construct, but it takes 1 cycle less because the LDA ZPHOLDA operation only takes 3 cycles, not the 4 cycles the PLA requires.

You could also just enter with some self-modifying code such as STA ARECOVER+1 (4 cycles) and exit with ARECOVER LDA #$00 (2 cycles... immediate value modified via STA ARECOVER+1).

All of the foregoing should, of course, highlight the folly of doing something stupidly unnecessary like this:

The superfluous use of diverse registers
LDX #<IRST6
LDY #>IRST6
STX $FFFE
STY $FFFF


The moral of the story being: never use extra registers unnecessarily!

Of course, where your interrupt handler must use 2 or all 3 registers, you can also apply the zero page holder variable method or the self-modifying code method, as per the technical notes in my Deep Winter Tech Demo article.

Similar posts:

Illegal Opcode SAX/AXS: A Practical Use

ORA: A Special Use in Branch Testing

____


PS: Don't forget to check the home page regularly for more articles like this.

And of course, kindly subscribe to my YouTube channel!

Last of all, for additional short snippets of content, check out the posts on my Ko-fi page.

Kodiak

Help Make Parallaxian Happen!

...and get special perks!

Progress on Parallaxian has slowed down since summer 2021 for several reasons, one of which has been the very low level of support from the C64 scene which has made it difficult to continue justifying to my family the long hours of hard work a project as complex as this requires.

Now, I understand these are difficult times and I admit I am not entitled to any support at all, but it really does encourage me to continue developing this sensational game when you make a Paypal donation.

And as a special thank you, all who do this can enjoy the following perks:

  • Your name credited in the game (unless you opt out of it if you have the same kind of incognito hermit tendencies I do).
  • Access to the ongoing beta-testing of the game (unless you would prefer not to see it before its release date).
  • The finished game on media (e.g. cartridge) for FREE one week before its release.