Close

Log #2: Stacks and subroutines

A project log for DIP-8 TTL Computer

Digital Information Processor - an 8-bit computer made out of 7400 series logic and some EEPROMs.

kaimackaimac 07/13/2022 at 13:540 Comments

When writing assembly, there are no rules about how to pass data to and from subroutines. Each subroutine can do it in a different way, for maximum efficiency. If there are just a few parameters, it probably makes sense to pass them in registers. Extra ones can go on the stack, although this can require a bit of juggling if the same stack is used for return addresses. Alternatively, data can be passed via a "parameter block" in-line with the code - check out this useful resource to see how that was done on the 6502.

Likewise, return values (and there can be more than one) can go in registers, on the stack, in fixed memory locations, or even in the form of the carry and zero status flags in the case of boolean values.

Here's an example of a hand-written subroutine in assembly. As the comment says, the inputs are the x and y registers, and the output is returned in b. It modifies the c register, so the caller would need to save its value on the stack, if it was important.

The "call" instruction is actually a macro that pushes the return address (the one after the call) and jumps to the given address. So it's really two instructions - 6 bytes in total. The "ret" instruction is a real instruction that pops a 16-bit value into the program counter.

The routine uses a "local" variable, mulbit, which is stored in a fixed location in memory.

            mov x, #56
            mov y, #39
            call mulxy
            ; b == 2184


;mulitply (8bit x 8bit = 16bit result)
; xy  inputs
; b   output
; c   clobbered

mulxy       mov b, #0
            mov cl, x
            mov ch, #0
            stl #1, mulbit
mloop       mov x, y        ; add if y & bit
            and x, [mulbit]
            jz  mnoadd
            add bl, cl      ; b += c
            adc bh, ch
mnoadd      add cl, cl      ; c *= 2
            adc ch, ch
            adr mulbit      ; bit *= 2
            ldx
            add x, x
            stx
            jcc mloop
            ret

mulbit      .byte 1

From assembly to a higher level

Eventually I'd like to implement a high-level language though (at least higher-level than assembly), and then we do need some rules - a calling convention. This will make heavy use of the stack, for arguments, return values, and each function's local variables. A stack-relative addressing mode, with which we can read and write values on the stack without pushing and popping them, is key, and that's why I have sp+imm8 and sp+imm16 addressing modes in the ISA.

Because the modes can only add a positive offset to the stack pointer, it makes more sense for the stack to grow down. If it grew up, you would need a negative offset to access a function's parameters.


So here's what the output might look like from a high-level compiler:

            ; call myfunc with 2 1-byte parameters
            push #12
            push #34
            push #return_addr   ; call
            jmp myfunc          ; call
            ...


myfunc      push x          ; callee saves x and y
            push y          
            sub sp, #32     ; allocate space for locals
            ...
            ldx sp+37       ; load parameter
            stx sp+3        ; store local variable
            ...
            call func2      ; call child function
            ...
            add sp, #32     ; deallocate locals
            pop y           ; restore x/y
            pop x    
            pop b
            add sp, #2      ; deallocate parameters
            jmp b           ; return

 This is all very preliminary, but it shows that a high level language could be implemented fairly efficiently.

Discussions