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:
fn_exit:
sep r0 ; return to caller, r8 again points to fn.
fn:
; do stuff.
lbr fn_exit ; go to "return" section.
; ----------
init_code:
ldi r8, fn ; set r8 to point to fn.
; ----------
main_code:
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.
If 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.
Like r8 and 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:
fn:
; 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 r4 and 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 scall and spush, to make things simpler and not clog up the source with unnecessary repeating code. The SCRT call function would be something like:
scall_exit:
sep r3
scall_fn:
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; then
- activated
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; and
- activate
r3 to effectively return to after the call.
This could be implemented as little more than:
sret_exit:
sep r3
sret_fn:
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].
sep r4
dw %1
endmacro
macro sret ; sret [returns from function].
sep r5
endmacro
macro spush %1 ; spush rN [pushes a register].
ghi %1 ; store each byte, decrementing sp.
stxd
glo %1
stxd ; sp always points at first free location.
endmacro
macro spop %1 ; spop rN [pops a register].
irx ; increment sp to skip free location.
ldxa ; get/store low byte, increment sp.
plo %1
ldx ; get/store high byte, no increment.
phi %1
endmacro
macro xfer %1 %2 ; xfer rX rY [transfers register rX to rY].
ghi %1 ; transfer both bytes.
phi %2
glo %2
plo r3
endmacro
ret?retis also a mnemonic for instruction 0x70 which is "return" and seems to do some kind of pop. http://www.ittybittycomputers.com/IttyBitty/ShortCor.htm – Omar and Lorraine Oct 18 '18 at 14:25