20

The RCA 1802 processor was used in a number of systems such as the ELF and COMX-35, and also apparently in the US space program (with its hardened variant).

From memory, it had a rather unusual set of instructions for doing function calls and returns. Can anyone detail how this actually worked in practice?

2 Answers 2

22

The 1802 used a method known as SCRT, the standard call and return technique. The chip was actually endowed with a full complement of sixteen 16-bit general-purpose registers(a), but no dedicated stack pointer or program counter. The SCRT was one method for doing calls using this limitation.

You could actually use any register as the program counter with the SEP Rn instruction (where n was a value 0..F indicating which of the registers was to be used). This basically said to start using that register for the program counter, leaving whatever register was previously being used with its last contents (one byte beyond the SEP instruction that switched the program counter over).

So, by using some of those sixteen registers in a dedicated manner, you could easily "emulate" the more conventional instructions found on other processors.

Note that the details below are from rather distant memories, they may not be exact (in terms of what registers were used for what, for example), but they should give you the basic idea.


There was a "usual" program counter (R3), and the R4 and R5 registers were set respectively to the addresses of the SCRT call and return functions. R2 was used as a stack pointer.

In order to call another function, you therefore encoded a SEP R4 followed by the address that you wanted to call. This immediately started using R4 as the program counter, leaving R3 pointing at the memory containing the address that you were calling.

The SCRT call code running at R4 would:

  • store the return address (R3, to be adjusted on return to skip over the address) onto the stack (controlled by R2);
  • load the address you wanted to call (pointed at by R3) into R3; then finally
  • execute SEP R3 to continue execution at that new address.

It would, of course, have to ensure R4 was set back to the start of the SCRT call function and, from memory, this was done by placing the SEP R3 instruction immediately before that function and then jumping to that as the final step. This would auto-magically leave R4 set to the correct value for next time.


Similarly, a SEP R5 (return) in your code would start running the return function which would:

  • pull the return address off the stack at R2, into R3, and adjust it to skip over the address.
  • do SEP R3 (using the same jump trickery mentioned above to ensure R5 once again pointed to the return function) to return to the original code.

So, in terms of implementation, the set-up was something like:

ScrtCallDone:
    sep r3
ScrtCall:
    ; code for doing reg/mem manipulations for call
    br ScrtCallDone

ScrtRetDone:
    sep r3
ScrtRet:
    ; code for doing reg/mem manipulations for ret
    br ScrtRetDone

macro fcall %address:
    sep    r4
    dw     address
endmacro

macro fret:
    sep    r5
endmacro

Then, in terms of what you would see in your code, it would be as simple as:

    fcall subfunc
    ; do other stuff then return

subfunc:
    ; do sub-function stuff
    fret

(a) This rather massive (at the time) register bank was actually one of the big selling features of the chip, despite the fact you immediately lost a large chunk of them (SCRT, DMA and interrupts, from memory). Truly the triumph of marketing over reality :-)

15
  • 2
    Ha! That's a beautiful hack. I designed a processor earlier this year that has its PC in a general purpose register and copied the last PC into another register whenever that register is modified, but if I'd thought of doing it this way it would have saved the need to update the register file twice in a single cycle! Although a call+return on my system is two instructions shorter...
    – Jules
    Oct 15, 2018 at 6:31
  • 2
    This description seems pretty similar to the IBM S/360 "Branch and link" and "Branch and link register" instructions, which used the same basic idea for subroutine calls. S/360 did have a program counter, but it didn't have a stack pointer, so the BAL or BALR instructions had to save the program counter in a user-defined location - either in another register, or in memory.
    – alephzero
    Oct 15, 2018 at 9:36
  • 1
    @alephzero, it is very similar. The admittedly minor stuff I did on the System z at IBM SW Labs actually reminded me of the 1802. See stackoverflow.com/questions/664744/… (the answer of mine that I sourced for this) for detail.
    – user6464
    Oct 15, 2018 at 9:54
  • 1
    @Wilson, probably not, I haven't actually used my old COMX-35 for about 30 years so my memory won't be perfect :-) Adjusted to use different macro name.
    – user6464
    Oct 19, 2018 at 0:23
  • 1
    The later 1804/1805/1806, and the "A" suffix versions of those, added actual stack-based call and return instruction. They used the formerly undefined/unusable 0x68 opcode as the prefix byte for a bunch of new two-byte instructions. However, these enhanced processors were mostly only seen in embedded systems, and not in many general-purpose computers. Part of the problem is that the "load mode" which allowed booting an 1802 from toggle switches or a keypad was removed in the enhanced processors.
    – Eric Smith
    Nov 15, 2018 at 23:41
4

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:

  1. 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.

  2. 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.

  3. The function has to know that r0 is being used as the calling PC since that's how it does the return.

  4. 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
1
  • That would be great for coroutines and continuations as well. Feb 26 at 10:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy