The RCA 1802 was a relatively strange beast in that it didn't have a dedicated program counter (or a dedicated stack pointer for that matter). Instead, it had the ability to select one of its 16-bit registers to act as the PC, and this could be changed at runtime with a simple instruction
SEP (set program counter).
This scheme allowed for efficiencies where, if a function was important enough (and the code was simple enough), a register could be dedicated to it as follows:
sep r0 ; return to caller, r8 again points to fn.
; do stuff.
lbr fn_exit ; go to "return" section.
ldi r8, fn ; set r8 to point to fn.
sep r8 ; call fn, leaving r0 set to next address.
The initialisation code sets
r8 to be the address of
fn. Then, at some point after that, your can simply
sep r8 and the processor will magically start running
fn. But there's a few caveats to that:
This is fine for calling one level down but doesn't really scale to general-purpose code where you need to call through many levels.
The function returns by branching to the code immediately before the start of the function. This trick ensures that, on exit,
r8 once again points to
fn, since it's updated to the next address as part of instruction fetch.
The function has to know that
r0 is being used as the calling PC since that's how it does the return.
r8 changes to something else, you better set it back to
fn before using this scheme again, or things are unlikely to end well :-)
So this is fine for very simple applications, as you may find in a dedicated-task system.
However, in order to scale up to an arbitrary number of function levels, a method called SCRT (standard call and return technique) was used. This technique dedicates a few registers for handling the call and return operations. For example, it may use:
r3 as the normal program counter;
r2 as the stack pointer;
r6 as a scratch register.
r4 dedicated to the call function address; and
r5 dedicated to the return function address.
fn in the code above, those two final registers were initialised to the address of the SCRT call and return functions respectively. And, again, those functions were written such that these registers would be the same value after they had done their job.
To define a function, you simply wrote the code you wanted and ended it by activating the SCRT return function. I'm using that "activate" term to indicate a
sep operation, so as to distinguish it from the higher level SCRT call. So something like this:
; do stuff.
sep r5 ; scrt return.
To call a function, you needed to activate the SCRT call function but also encode the address of the function you wanted to call(1):
sep r4 ; scrt call
defw fn ; with the address to call.
The functions referenced by
r5 used the same trick as in the simple code described above (see
fn_exit). They finished by branching to the code immediately before the function to ensure the register was once again set correctly. But the "meat" of each was adapted for the more general-purpose calling convention.
As per my earlier footnote(1), this code uses utility macros, such as
spush, to make things simpler and not clog up the source with unnecessary repeating code. The SCRT call function would be something like:
lda r3 ; get high byte of target to r6 high,
phi r6 ; incrementing r3.
lda r3 ; same for low byte to r6 low,
plo r6 ; r3 now at return address.
spush r3 ; push that return address.
xfer r6 r3 ; prepare r3 to be target,
lbr scall_exit ; and activate it.
In other words, it:
- saved the target address into scratch register;
- stored return address onto the stack;
- loaded scratch register into
r3 to effectively jump to the target address.
Returning would be the opposite actions, but rather simpler than the call operation:
- retrieve the return address from the stack into
r3 to effectively return to after the call.
This could be implemented as little more than:
spop r3 ; prepare r3 to be return address,
lbr sret_exit ; and activate it.
(1) If you had a decent assembler, you could create macros to do the heavy lifting for you, as follows. I've also included some utility functions as well given the simplicity of the RCA 1802 instruction set:
macro scall %1 ; scall <address> [calls a function].
macro sret ; sret [returns from function].
macro spush %1 ; spush rN [pushes a register].
ghi %1 ; store each byte, decrementing sp.
stxd ; sp always points at first free location.
macro spop %1 ; spop rN [pops a register].
irx ; increment sp to skip free location.
ldxa ; get/store low byte, increment sp.
ldx ; get/store high byte, no increment.
macro xfer %1 %2 ; xfer rX rY [transfers register rX to rY].
ghi %1 ; transfer both bytes.