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:
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:
- It stores the contents of the Accumulator out of harm's way for later retrieval at the end of the interrupt.
- It stores the contents of the X-register out of harm's way for later retrieval at the end of the interrupt.
- 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:
; 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:
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:
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!
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 regular
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.