Now suppose that somewhere in the execution of the subroutines in the diagram in section 2, a piece of hardware generates an interrupt, either on the IRQ or the NMI (Interrupt ReQuest or Non-Maskable Interrupt) processor input pin, and it's something that's time-critical and you need the processor to service it immediately. Interrupts by nature are asynchronous, and you don't know where the program counter will be when an interrupt is generated. (See my article on interrupts—and enjoy my out-of-date cartoons! ;-) The article starts out with what interrupts are, and are not, with real-life examples in non-technical jargon, then gets to the nuts and bolts, with practical examples.)
Similar to the nesting of subroutines in section 2, the interrupt service requires that the program counter go to another part of program memory, execute a routine, and when it's done, come back and pick back up where it left off. This time, however, since there was no way to plan where the program counter would be when the interrupt hit, we have to make sure that we save and restore the processor's register contents so there won't be changes that foul up the original program when the interrupt service is finished. That's ok though, as the stack can save more than just the return address. An automatic part of the 6502's interrupt sequence is to also push the status register's contents onto the stack for later restoration. The interrupt-service routine (ISR) needs to also save A, X, and/or Y, whichever ones it uses (which is not necessarily all of them), again by pushing them onto the stack, then pulling them back off in reverse order before the RTI (ReTurn-from-Interrupt) instruction.
So expanding on the scenario in section 2 where we had the example that the stack pointer (register S) was at $EF before the code entered
the diagram in that section, we can chart the known stack contents as follows. (I say "known" because we don't know what was in $EF to
$FF inclusive, so we won't chart those bytes.)
------------------stack memory location------------------- S $1EF $1EE $1ED $1EC $1EB $1EA $1E9 $1E8 $1E7 $1E6 start: $EF ? ? ? ? ? ? ? ? ? ? at a: $ED $19 $15 ? ? ? ? ? ? ? ? at b: $EB $19 $15 $26 $32 ? ? ? ? ? ? at c: $E9 $19 $15 $26 $32 $2F $43 ? ? ? ? after 1st RTS: $EB $19 $15 $26 $32 ($2F) ($43) ? ? ? ? (and now the interrupt in the diagram above hits:) at d: $E8 $19 $15 $26 $32 $2F $92 $81 ? ? ? [Note 1] at e: $E7 $19 $15 $26 $32 $2F $92 $81 $49 ? ? at f: $E6 $19 $15 $26 $32 $2F $92 $81 $49 $06 ? at g: $E6 $19 $15 $26 $32 $2F $92 $81 $49 $06 ? at h: $E7 $19 $15 $26 $32 $2F $92 $81 $49 ($06) ? at i: $E8 $19 $15 $26 $32 $2F $92 $81 ($49) ($06) ? after RTI: $EB $19 $15 $26 $32 ($2F) ($92) ($81) ($49) ($06) ? after 2nd RTS: $ED $19 $15 ($26) ($32) ($2F) ($92) ($81) ($49) ($06) ? after 3rd RTS: $EF ($19) ($15) ($26) ($32) ($2F) ($92) ($81) ($49) ($06) ? Note[1] $2F92 is the actual address for RTI to return to (unlike addr-1 which RTS needs), and $81 is the status for it to restore. Note that the old contents of $1EB and $1EA get overwritten.
Again, the numbers in parentheses are still there, but that part of the stack memory is freed up.
Note that in the particular example, the ISR did not use Y, so there was no point in saving and restoring it, using PHY and PLY. In fact, it would be possible to have a minimal ISR that does not use A or X either, for example if all it had to do was clear the interrupt with the BIT instruction (simply by reading the I/O IC's status register) and zero a RAM variable with STZ or increment it with INC. In that case the only thing that would need saving and restoring (besides the return address) is the status register P, and the interrupt sequence and RTI save and restore that automatically anyway.
As a parenthetical note: Since the processor status register P is automatically saved and restored, there is no need to start an ISR with PHP and end it with PLP. There is also no need to set the interrupt-disable flag I at the beginning of an interrupt (using SEI), as that is automatic too, part of the interrupt sequence, immediately after the status is pushed. And one more: do not re-enable interrupts (with CLI) just before RTI. Why? Because if another interrupt happens to already be pending, you'll go into the ISR again without exiting the first instance of it. In an extreme case of this happening over and over, you'll overflow the stack and crash, whereas otherwise, if you can catch up, there will be no crash.
Having said that, there could be a good reason to re-enable interrupts before exiting the ISR, like to allow a high-priority quick-to-service interrupt to interrupt the service of a lower-priority one that may take longer to service. The CLI won't be right at the end though, and you would need to be careful to allow only the right things to interrupt and to avoid the potential problem mentioned above. If it's such a high priority, you can put it on the NMI line.
Can the interrupt-service routine call a subroutine? Sure! And can an interrupt cut in on a subroutine? Again,
sure—in fact that's what we just did above. Can an interrupt interrupt the servicing of another interrupt? Again the
answer is yes, although either the first interrupt-service routine (ISR) would have to re-enable interrupts before it's done (ie, not
just wait for the RTI to do it), or the second interrupt would have to come in
on NMI.
2. subroutine return addresses & nesting <--Previous |
Next--> 4. virtual stacks
last updated Mar 5, 2022